diff --git a/api/certadd/insert.go b/api/certadd/insert.go index d7b5e825b..245911d29 100644 --- a/api/certadd/insert.go +++ b/api/certadd/insert.go @@ -2,6 +2,7 @@ package certadd import ( "bytes" + "database/sql" "encoding/hex" "encoding/json" "io/ioutil" @@ -14,6 +15,7 @@ import ( "github.com/cloudflare/cfssl/errors" "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/ocsp" + "github.com/jmoiron/sqlx/types" "encoding/base64" @@ -48,14 +50,19 @@ func NewHandler(dbAccessor certdb.Accessor, signer ocsp.Signer) http.Handler { // AddRequest describes a request from a client to insert a // certificate into the database. type AddRequest struct { - Serial string `json:"serial_number"` - AKI string `json:"authority_key_identifier"` - CALabel string `json:"ca_label"` - Status string `json:"status"` - Reason int `json:"reason"` - Expiry time.Time `json:"expiry"` - RevokedAt time.Time `json:"revoked_at"` - PEM string `json:"pem"` + Serial string `json:"serial_number"` + AKI string `json:"authority_key_identifier"` + CALabel string `json:"ca_label"` + Status string `json:"status"` + Reason int `json:"reason"` + Expiry time.Time `json:"expiry"` + RevokedAt time.Time `json:"revoked_at"` + PEM string `json:"pem"` + IssuedAt *time.Time `json:"issued_at"` + NotBefore *time.Time `json:"not_before"` + MetadataJSON types.JSONText `json:"metadata"` + SansJSON types.JSONText `json:"sans"` + CommonName string `json:"common_name"` } // Map of valid reason codes @@ -113,6 +120,10 @@ func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) error { return errors.NewBadRequestString("The provided certificate is empty") } + if req.Expiry.IsZero() { + return errors.NewBadRequestString("Expiry is required but not provided") + } + // Parse the certificate and validate that it matches cert, err := helpers.ParseCertificatePEM([]byte(req.PEM)) if err != nil { @@ -120,7 +131,7 @@ func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) error { } serialBigInt := new(big.Int) - if _, success := serialBigInt.SetString(req.Serial, 16); !success { + if _, success := serialBigInt.SetString(req.Serial, 10); !success { return errors.NewBadRequestString("Unable to parse serial key of request") } @@ -137,15 +148,24 @@ func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) error { return errors.NewBadRequestString("Authority key identifier of request and certificate do not match") } + if req.Expiry != cert.NotAfter { + return errors.NewBadRequestString("Expiry of request and certificate do not match") + } + cr := certdb.CertificateRecord{ - Serial: req.Serial, - AKI: req.AKI, - CALabel: req.CALabel, - Status: req.Status, - Reason: req.Reason, - Expiry: req.Expiry, - RevokedAt: req.RevokedAt, - PEM: req.PEM, + Serial: req.Serial, + AKI: req.AKI, + CALabel: req.CALabel, + Status: req.Status, + Reason: req.Reason, + Expiry: req.Expiry, + RevokedAt: req.RevokedAt, + PEM: req.PEM, + IssuedAt: req.IssuedAt, + NotBefore: req.NotBefore, + MetadataJSON: req.MetadataJSON, + SANsJSON: req.SansJSON, + CommonName: sql.NullString{String: req.CommonName, Valid: req.CommonName != ""}, } err = h.dbAccessor.InsertCertificate(cr) diff --git a/api/certadd/insert_test.go b/api/certadd/insert_test.go index 41b04f74a..ec855024f 100644 --- a/api/certadd/insert_test.go +++ b/api/certadd/insert_test.go @@ -73,6 +73,7 @@ func makeCertificate() (serialNumber *big.Int, cert *x509.Certificate, pemBytes Organization: []string{"Cornell CS 5152"}, }, AuthorityKeyId: []byte{42, 42, 42, 42}, + NotAfter: time.Now(), } cert = &template @@ -91,9 +92,9 @@ func makeCertificate() (serialNumber *big.Int, cert *x509.Certificate, pemBytes Subject: pkix.Name{ Organization: []string{"Cornell CS 5152"}, }, - AuthorityKeyId: []byte{42, 42, 42, 42}, - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - IsCA: true, + AuthorityKeyId: []byte{42, 42, 42, 42}, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + IsCA: true, BasicConstraintsValid: true, } issuerBytes, err := x509.CreateCertificate(rand.Reader, &issuerTemplate, &issuerTemplate, &privKey.PublicKey, privKey) @@ -153,10 +154,11 @@ func TestInsertValidCertificate(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "good", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusOK { @@ -179,7 +181,7 @@ func TestInsertValidCertificate(t *testing.T) { t.Fatal("Could not parse returned OCSP response", err) } - ocsps, err := dbAccessor.GetOCSP(serialNumber.Text(16), hex.EncodeToString(cert.AuthorityKeyId)) + ocsps, err := dbAccessor.GetOCSP(serialNumber.Text(10), hex.EncodeToString(cert.AuthorityKeyId)) if err != nil { t.Fatal(err) } @@ -223,6 +225,7 @@ func TestInsertMissingSerial(t *testing.T) { "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "good", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -236,16 +239,41 @@ func TestInsertMissingAKI(t *testing.T) { t.Fatal(err) } - serialNumber, _, pemBytes, signer, err := makeCertificate() + serialNumber, cert, pemBytes, signer, err := makeCertificate() if err != nil { t.Fatal(err) } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "status": "good", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), + }) + + if resp.StatusCode != http.StatusBadRequest { + t.Fatal("Expected HTTP Bad Request", resp.StatusCode, string(body)) + } +} + +func TestInsertMissingExpiry(t *testing.T) { + dbAccessor, err := prepDB() + if err != nil { + t.Fatal(err) + } + + serialNumber, cert, pemBytes, signer, err := makeCertificate() + + if err != nil { + t.Fatal(err) + } + + resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ + "serial_number": serialNumber.Text(10), + "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), + "status": "good", + "pem": string(pemBytes), }) if resp.StatusCode != http.StatusBadRequest { @@ -266,9 +294,10 @@ func TestInsertMissingPEM(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "good", + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -293,6 +322,7 @@ func TestInsertInvalidSerial(t *testing.T) { "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "good", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -306,17 +336,18 @@ func TestInsertInvalidAKI(t *testing.T) { t.Fatal(err) } - serialNumber, _, pemBytes, signer, err := makeCertificate() + serialNumber, cert, pemBytes, signer, err := makeCertificate() if err != nil { t.Fatal(err) } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": "this is not an AKI", "status": "good", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -337,10 +368,11 @@ func TestInsertInvalidStatus(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "invalid", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -361,10 +393,36 @@ func TestInsertInvalidPEM(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "good", "pem": "this is not a PEM certificate", + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), + }) + + if resp.StatusCode != http.StatusBadRequest { + t.Fatal("Expected HTTP Bad Request, got", resp.StatusCode, string(body)) + } +} + +func TestInsertInvalidExpiry(t *testing.T) { + dbAccessor, err := prepDB() + if err != nil { + t.Fatal(err) + } + + serialNumber, cert, pemBytes, signer, err := makeCertificate() + + if err != nil { + t.Fatal(err) + } + + resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ + "serial_number": serialNumber.Text(10), + "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), + "status": "good", + "pem": string(pemBytes), + "expiry": "this is not an expiry", }) if resp.StatusCode != http.StatusBadRequest { @@ -385,10 +443,11 @@ func TestInsertWrongSerial(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": big.NewInt(1).Text(16), + "serial_number": big.NewInt(1).Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "good", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -402,6 +461,31 @@ func TestInsertWrongAKI(t *testing.T) { t.Fatal(err) } + serialNumber, cert, pemBytes, signer, err := makeCertificate() + + if err != nil { + t.Fatal(err) + } + + resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ + "serial_number": serialNumber.Text(10), + "authority_key_identifier": hex.EncodeToString([]byte{7, 7}), + "status": "good", + "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), + }) + + if resp.StatusCode != http.StatusBadRequest { + t.Fatal("Expected HTTP Bad Request", resp.StatusCode, string(body)) + } +} + +func TestInsertWrongExpiry(t *testing.T) { + dbAccessor, err := prepDB() + if err != nil { + t.Fatal(err) + } + serialNumber, _, pemBytes, signer, err := makeCertificate() if err != nil { @@ -409,10 +493,11 @@ func TestInsertWrongAKI(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString([]byte{7, 7}), "status": "good", "pem": string(pemBytes), + "expiry": time.Now().UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusBadRequest { @@ -433,18 +518,19 @@ func TestInsertRevokedCertificate(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "revoked", "pem": string(pemBytes), "revoked_at": time.Now(), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), }) if resp.StatusCode != http.StatusOK { t.Fatal("Expected HTTP OK", resp.StatusCode, string(body)) } - ocsps, err := dbAccessor.GetOCSP(serialNumber.Text(16), hex.EncodeToString(cert.AuthorityKeyId)) + ocsps, err := dbAccessor.GetOCSP(serialNumber.Text(10), hex.EncodeToString(cert.AuthorityKeyId)) if err != nil { t.Fatal(err) } @@ -477,10 +563,11 @@ func TestInsertRevokedCertificateWithoutTime(t *testing.T) { } resp, body := makeRequest(t, dbAccessor, signer, map[string]interface{}{ - "serial_number": serialNumber.Text(16), + "serial_number": serialNumber.Text(10), "authority_key_identifier": hex.EncodeToString(cert.AuthorityKeyId), "status": "revoked", "pem": string(pemBytes), + "expiry": cert.NotAfter.UTC().Format(time.RFC3339), // Omit RevokedAt }) diff --git a/cli/serve/serve.go b/cli/serve/serve.go index 1a9fbf28a..716751904 100644 --- a/cli/serve/serve.go +++ b/cli/serve/serve.go @@ -16,6 +16,7 @@ import ( rice "github.com/GeertJohan/go.rice" "github.com/cloudflare/cfssl/api" "github.com/cloudflare/cfssl/api/bundle" + "github.com/cloudflare/cfssl/api/certadd" "github.com/cloudflare/cfssl/api/certinfo" "github.com/cloudflare/cfssl/api/crl" "github.com/cloudflare/cfssl/api/gencrl" @@ -251,6 +252,10 @@ var endpoints = map[string]func() (http.Handler, error){ "health": func() (http.Handler, error) { return health.NewHealthCheck(), nil }, + + "certadd": func() (http.Handler, error) { + return certadd.NewHandler(certsql.NewAccessor(db), nil), nil + }, } // registerHandlers instantiates various handlers and associate them to corresponding endpoints. diff --git a/cli/serve/serve_test.go b/cli/serve/serve_test.go index 118e8498e..bdaeca9bc 100644 --- a/cli/serve/serve_test.go +++ b/cli/serve/serve_test.go @@ -47,6 +47,7 @@ func TestServe(t *testing.T) { expected[v1APIPath("newkey")] = http.StatusMethodNotAllowed expected[v1APIPath("bundle")] = http.StatusMethodNotAllowed expected[v1APIPath("certinfo")] = http.StatusMethodNotAllowed + expected[v1APIPath("certadd")] = http.StatusMethodNotAllowed // POST-only endpoints should return '400 Bad Request' expected[v1APIPath("scan")] = http.StatusBadRequest