From 1431e72e96552659d283f53d07a70416fb04c70e Mon Sep 17 00:00:00 2001 From: Pawel Boguslawski Date: Sat, 2 Sep 2023 16:39:35 +0200 Subject: [PATCH] Allow API client to provide serial and detect duplicate entry error This mod adds access to `ClientProvidesSerialNumbers` cert profile field from JSON profile config with `client_provides_serial_numbers` option. When enabled, cert serial must be provided by API client in request. This mod adds also specific API error code in case of sign request failure caused by already taken serial number. For MySQL/MariaDB and SQLite only (to be done separately for PostgreSQL by devs using this DB engine). Author-Change-Id: IB#1137304 Signed-off-by: Pawel Boguslawski --- certdb/sql/database_accessor.go | 35 ++++++++++++++++++++++++++++++++- config/config.go | 3 ++- errors/error.go | 3 +++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/certdb/sql/database_accessor.go b/certdb/sql/database_accessor.go index 63b0db8bf..d9ff78198 100644 --- a/certdb/sql/database_accessor.go +++ b/certdb/sql/database_accessor.go @@ -3,10 +3,13 @@ package sql import ( "errors" "fmt" + "regexp" "time" "github.com/cloudflare/cfssl/certdb" cferr "github.com/cloudflare/cfssl/errors" + "github.com/go-sql-driver/mysql" + "github.com/mattn/go-sqlite3" "github.com/jmoiron/sqlx" "github.com/kisielk/sqlstruct" @@ -76,7 +79,37 @@ var _ certdb.Accessor = &Accessor{} func wrapSQLError(err error) error { if err != nil { - return cferr.Wrap(cferr.CertStoreError, cferr.Unknown, err) + + reason := cferr.Unknown + + // Use detailed reason on unique constraint errors (i.e. will allow API client + // to detect already used cert serial in DB when API client is + // allowed to provide cert serial on cert singing). We don't detect this + // kind of problems by querying table for exisitng key before insert/update + // to avoid races. Unique constraint errors have different codes in different + // DB engines so must be detected separately. + + // MySQL/MariaDB + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + reason = cferr.DuplicateEntry + } + + // SQLite + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) && (sqliteErr.Code == sqlite3.ErrConstraint) { + + // Parsing error message is probably the only way to detect duplicate key + // errors in SQLite now... + if regexp.MustCompile(`(^|\s)UNIQUE constraint failed .*`).MatchString(err.Error()) { + reason = cferr.DuplicateEntry + } + } + + // PostgresSQL + // TBD. See also: https://github.com/go-gorm/gorm/issues/4135 + + return cferr.Wrap(cferr.CertStoreError, reason, err) } return nil } diff --git a/config/config.go b/config/config.go index f97d64698..36baf10a5 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ import ( "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/log" ocspConfig "github.com/cloudflare/cfssl/ocsp/config" + // empty import of zlint/v3 required to have lints registered. _ "github.com/zmap/zlint/v3" "github.com/zmap/zlint/v3/lint" @@ -121,7 +122,7 @@ type SigningProfile struct { CSRWhitelist *CSRWhitelist NameWhitelist *regexp.Regexp ExtensionWhitelist map[string]bool - ClientProvidesSerialNumbers bool + ClientProvidesSerialNumbers bool `json:"client_provides_serial_numbers"` // LintRegistry is the collection of lints that should be used if // LintErrLevel is configured. By default all ZLint lints are used. If // ExcludeLints or ExcludeLintSources are set then this registry will be diff --git a/errors/error.go b/errors/error.go index 9715a7cfb..3a3f1573d 100644 --- a/errors/error.go +++ b/errors/error.go @@ -210,6 +210,9 @@ const ( // RecordNotFound occurs when a SQL query targeting on one unique // record failes to update the specified row in the table. RecordNotFound + // DuplicateEntry occurs when SQL query tries to insert or update + // using key that must be unique in db table but already exists there. + DuplicateEntry ) // The error interface implementation, which formats to a JSON object string.