cloudflare/cfssl_trust
Publicmirrored from https://github.com/cloudflare/cfssl_trustAvailable
model/certdb/certificate.go
611lines · modecode
| 1 | // Package certdb contains Go definitions for the database |
| 2 | // representation of certificates, as well as associated code for |
| 3 | // putting it into the database. |
| 4 | package certdb |
| 5 | |
| 6 | import ( |
| 7 | "crypto/x509" |
| 8 | "database/sql" |
| 9 | "errors" |
| 10 | "fmt" |
| 11 | "math/big" |
| 12 | "os" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/cloudflare/cfssl/signer" |
| 16 | ) |
| 17 | |
| 18 | // Finalize finishes a transaction, committing it if needed or rolling |
| 19 | // back on error. |
| 20 | func Finalize(err *error, tx *sql.Tx) { |
| 21 | if *err == nil { |
| 22 | *err = tx.Commit() |
| 23 | if *err != nil { |
| 24 | fmt.Fprintf(os.Stderr, "[!] failed to commit transaction: %s\n", *err) |
| 25 | os.Exit(1) |
| 26 | } |
| 27 | } else { |
| 28 | tx.Rollback() |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | // FindCertificateBySKI returns all the certificates with the given |
| 33 | // SKI. |
| 34 | func FindCertificateBySKI(db *sql.DB, ski string) ([]*Certificate, error) { |
| 35 | tx, err := db.Begin() |
| 36 | if err != nil { |
| 37 | return nil, err |
| 38 | } |
| 39 | defer Finalize(&err, tx) |
| 40 | |
| 41 | var certificates []*Certificate |
| 42 | rows, err := tx.Query(` |
| 43 | SELECT aki, serial, not_before, not_after, raw |
| 44 | FROM certificates |
| 45 | WHERE ski=?`, |
| 46 | ski) |
| 47 | if err != nil { |
| 48 | return nil, err |
| 49 | } |
| 50 | |
| 51 | for rows.Next() { |
| 52 | cert := &Certificate{ |
| 53 | SKI: ski, |
| 54 | } |
| 55 | |
| 56 | err = rows.Scan(&cert.AKI, &cert.Serial, &cert.NotBefore, |
| 57 | &cert.NotAfter, &cert.Raw) |
| 58 | if err != nil { |
| 59 | return nil, err |
| 60 | } |
| 61 | |
| 62 | cert.cert, err = x509.ParseCertificate(cert.Raw) |
| 63 | if err != nil { |
| 64 | return nil, err |
| 65 | } |
| 66 | |
| 67 | certificates = append(certificates, cert) |
| 68 | } |
| 69 | |
| 70 | return certificates, nil |
| 71 | } |
| 72 | |
| 73 | // AllCertificates loads all the certificates in the database. |
| 74 | func AllCertificates(tx *sql.Tx) ([]*Certificate, error) { |
| 75 | rows, err := tx.Query("SELECT * FROM certificates") |
| 76 | if err != nil { |
| 77 | return nil, err |
| 78 | } |
| 79 | |
| 80 | var certificates []*Certificate |
| 81 | for rows.Next() { |
| 82 | cert := &Certificate{} |
| 83 | err = rows.Scan(&cert.SKI, &cert.AKI, &cert.Serial, &cert.NotBefore, |
| 84 | &cert.NotAfter, &cert.Raw) |
| 85 | if err != nil { |
| 86 | return nil, err |
| 87 | } |
| 88 | |
| 89 | cert.cert, err = x509.ParseCertificate(cert.Raw) |
| 90 | if err != nil { |
| 91 | return nil, err |
| 92 | } |
| 93 | |
| 94 | certificates = append(certificates, cert) |
| 95 | } |
| 96 | |
| 97 | return certificates, err |
| 98 | } |
| 99 | |
| 100 | // Table provides an interface for mapping a struct to a table in the |
| 101 | // database. |
| 102 | type Table interface { |
| 103 | // Insert stores a value in the database; it doesn't check |
| 104 | // whether the value exists in the database already and isn't |
| 105 | // idempotent --- calling it twice on the same value will |
| 106 | // likely violate UNIQUE constraints. |
| 107 | Insert(tx *sql.Tx) error |
| 108 | |
| 109 | // Select fills in the value given certain primary fields |
| 110 | // being filled in. The function comment for each struct's |
| 111 | // implementation should note which fields should be filled in |
| 112 | // prior to calling this. It should also return sql.ErrNoRows |
| 113 | // if the item doesn't exist in the database. |
| 114 | Select(tx *sql.Tx) error |
| 115 | |
| 116 | // Not used yet, but might be useful in the future. |
| 117 | // Delete(tx *sql.Tx) error |
| 118 | // Update(tx *sql.Tx) error |
| 119 | } |
| 120 | |
| 121 | // Ensure ensures the value is present in the database. It calls |
| 122 | // Select, and if no rows are returned, it calls Insert. The boolean |
| 123 | // will be true if the value was inserted. This value is meaningless |
| 124 | // if err is non-nil. |
| 125 | func Ensure(table Table, tx *sql.Tx) (bool, error) { |
| 126 | var inserted bool |
| 127 | err := table.Select(tx) |
| 128 | if err == sql.ErrNoRows { |
| 129 | err = table.Insert(tx) |
| 130 | inserted = true |
| 131 | } |
| 132 | return inserted, err |
| 133 | } |
| 134 | |
| 135 | // Certificate models the certificate table. |
| 136 | type Certificate struct { |
| 137 | SKI string |
| 138 | AKI string |
| 139 | Serial []byte |
| 140 | NotBefore int64 |
| 141 | NotAfter int64 |
| 142 | Raw []byte |
| 143 | cert *x509.Certificate |
| 144 | } // UNIQUE(ski, serial) |
| 145 | |
| 146 | // Insert stores the Certificate in the database. |
| 147 | func (cert *Certificate) Insert(tx *sql.Tx) error { |
| 148 | _, err := tx.Exec(`INSERT INTO certificates (ski, aki, serial, not_before, not_after, raw) values (?, ?, ?, ?, ?, ?)`, cert.SKI, cert.AKI, cert.Serial, cert.NotBefore, cert.NotAfter, cert.Raw) |
| 149 | return err |
| 150 | } |
| 151 | |
| 152 | // Select requires the SKI and Serial fields to be filled in. |
| 153 | func (cert *Certificate) Select(tx *sql.Tx) error { |
| 154 | row := tx.QueryRow(`SELECT aki, not_before, not_after, raw FROM certificates WHERE ski=? and serial=?`, cert.SKI, cert.Serial) |
| 155 | err := row.Scan(&cert.AKI, &cert.NotBefore, &cert.NotAfter, &cert.Raw) |
| 156 | if err != nil { |
| 157 | return err |
| 158 | } |
| 159 | |
| 160 | cert.cert, err = x509.ParseCertificate(cert.Raw) |
| 161 | return err |
| 162 | } |
| 163 | |
| 164 | // Releases looks up all the releases for a certificate. |
| 165 | func (cert *Certificate) Releases(tx *sql.Tx) ([]*Release, error) { |
| 166 | var releases []*Release |
| 167 | |
| 168 | rows, err := tx.Query("SELECT release FROM roots WHERE ski=? AND serial=?", cert.SKI, cert.Serial) |
| 169 | if err != nil { |
| 170 | return nil, err |
| 171 | } |
| 172 | |
| 173 | for rows.Next() { |
| 174 | rel := &Release{ |
| 175 | Bundle: "ca", |
| 176 | } |
| 177 | |
| 178 | err = rows.Scan(&rel.Version) |
| 179 | if err != nil { |
| 180 | return nil, err |
| 181 | } |
| 182 | |
| 183 | releases = append(releases, rel) |
| 184 | } |
| 185 | rows.Close() |
| 186 | |
| 187 | rows, err = tx.Query("SELECT release FROM intermediates WHERE ski=? AND serial=?", cert.SKI, cert.Serial) |
| 188 | if err != nil { |
| 189 | return nil, err |
| 190 | } |
| 191 | |
| 192 | for rows.Next() { |
| 193 | rel := &Release{ |
| 194 | Bundle: "int", |
| 195 | } |
| 196 | |
| 197 | err = rows.Scan(&rel.Version) |
| 198 | if err != nil { |
| 199 | return nil, err |
| 200 | } |
| 201 | |
| 202 | releases = append(releases, rel) |
| 203 | } |
| 204 | |
| 205 | for _, rel := range releases { |
| 206 | err = rel.Select(tx) |
| 207 | if err != nil { |
| 208 | return nil, err |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | return releases, nil |
| 213 | } |
| 214 | |
| 215 | // Revoked returns true if the certificate was revoked before the |
| 216 | // timestamp passed in. |
| 217 | func (cert *Certificate) Revoked(tx *sql.Tx, when int64) (bool, error) { |
| 218 | if err := cert.Select(tx); err != nil { |
| 219 | return true, err |
| 220 | } |
| 221 | |
| 222 | var count int |
| 223 | row := tx.QueryRow(`SELECT count(*) FROM revocations WHERE ski=? AND revoked_at <= ?`, cert.SKI, when) |
| 224 | err := row.Scan(&count) |
| 225 | if err != nil { |
| 226 | return true, err |
| 227 | } |
| 228 | |
| 229 | return count > 0, nil |
| 230 | } |
| 231 | |
| 232 | // Revoke marks the certificate as revoked. |
| 233 | func (cert *Certificate) Revoke(tx *sql.Tx, mechanism, reason string, when int64) error { |
| 234 | if err := cert.Select(tx); err != nil { |
| 235 | return err |
| 236 | } |
| 237 | |
| 238 | rev := &Revocation{ |
| 239 | SKI: cert.SKI, |
| 240 | RevokedAt: when, |
| 241 | Mechanism: mechanism, |
| 242 | Reason: reason, |
| 243 | } |
| 244 | |
| 245 | // We ignore the inserted value because if it returns false, that means |
| 246 | // the certificate has already been revoked. |
| 247 | _, err := Ensure(rev, tx) |
| 248 | return err |
| 249 | } |
| 250 | |
| 251 | // X509 returns the *crypto/x509.Certificate from the certificate. |
| 252 | func (cert *Certificate) X509() *x509.Certificate { |
| 253 | return cert.cert |
| 254 | } |
| 255 | |
| 256 | var nullSerial = big.NewInt(0) |
| 257 | |
| 258 | // NewCertificate creates a Certificate from a crypto/x509 Certificate |
| 259 | // structure. |
| 260 | func NewCertificate(cert *x509.Certificate) *Certificate { |
| 261 | c := &Certificate{ |
| 262 | SKI: fmt.Sprintf("%x", cert.SubjectKeyId), |
| 263 | AKI: fmt.Sprintf("%x", cert.AuthorityKeyId), |
| 264 | Serial: cert.SerialNumber.Bytes(), |
| 265 | NotBefore: cert.NotBefore.Unix(), |
| 266 | NotAfter: cert.NotAfter.Unix(), |
| 267 | Raw: cert.Raw, |
| 268 | } |
| 269 | |
| 270 | // Workaround the NOT NULL constraint. |
| 271 | if cert.SerialNumber.Cmp(nullSerial) == 0 { |
| 272 | c.Serial = []byte{0} |
| 273 | } |
| 274 | |
| 275 | // Work around the fact that many early CA roots don't have an |
| 276 | // SKI. This uses the method found in RFC 5280 Section 4.2.1.2 |
| 277 | // (1). |
| 278 | if c.SKI == "" { |
| 279 | ski, err := signer.ComputeSKI(cert) |
| 280 | if err != nil { |
| 281 | panic("invalid public key in root certificate") |
| 282 | } |
| 283 | |
| 284 | c.SKI = fmt.Sprintf("%x", ski) |
| 285 | } |
| 286 | |
| 287 | c.cert = cert |
| 288 | |
| 289 | return c |
| 290 | } |
| 291 | |
| 292 | // AIA models the aia table. |
| 293 | type AIA struct { |
| 294 | SKI string // Primary key. |
| 295 | URL string |
| 296 | } |
| 297 | |
| 298 | // Insert stores the release in the database. |
| 299 | func (aia *AIA) Insert(tx *sql.Tx) error { |
| 300 | _, err := tx.Exec(`INSERT INTO aia (ski, url) values (?, ?)`, aia.SKI, aia.URL) |
| 301 | return err |
| 302 | } |
| 303 | |
| 304 | // Select requires the SKI field to be filled in. |
| 305 | func (aia *AIA) Select(tx *sql.Tx) error { |
| 306 | row := tx.QueryRow(`SELECT url FROM aia WHERE ski=?`, aia.SKI) |
| 307 | err := row.Scan(&aia.URL) |
| 308 | if err != nil { |
| 309 | return err |
| 310 | } |
| 311 | |
| 312 | return nil |
| 313 | } |
| 314 | |
| 315 | // NewAIA populates an AIA structure from a Certificate. |
| 316 | func NewAIA(cert *Certificate) *AIA { |
| 317 | if len(cert.cert.IssuingCertificateURL) == 0 { |
| 318 | return nil |
| 319 | } |
| 320 | |
| 321 | // Arbitrary choice: store the first HTTP URL. We can always |
| 322 | // look up the other URLs later and replace this one if |
| 323 | // need be. |
| 324 | return &AIA{ |
| 325 | SKI: cert.AKI, |
| 326 | URL: cert.cert.IssuingCertificateURL[0], |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | func validBundle(bundle string) bool { |
| 331 | switch bundle { |
| 332 | case "ca", "int": |
| 333 | return true |
| 334 | default: |
| 335 | return false |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | func tableForBundle(bundle string) string { |
| 340 | switch bundle { |
| 341 | case "ca": |
| 342 | return "root" |
| 343 | case "int": |
| 344 | return "intermediate" |
| 345 | default: |
| 346 | // The bundle should have been validated by here; it's better |
| 347 | // to panic and stop the world (generating a stack trace) |
| 348 | // than to continue. |
| 349 | panic("certdb: bundle should have been validated before the table selection") |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | // Release models the root_releases and intermediate_releases tables. |
| 354 | type Release struct { |
| 355 | Bundle string // Is this a CA or intermediate release? |
| 356 | Version string |
| 357 | ReleasedAt int64 |
| 358 | } |
| 359 | |
| 360 | func (r *Release) validBundle() bool { |
| 361 | return validBundle(r.Bundle) |
| 362 | } |
| 363 | |
| 364 | func (r *Release) table() string { |
| 365 | return tableForBundle(r.Bundle) |
| 366 | } |
| 367 | |
| 368 | func (r *Release) errInvalidBundle() error { |
| 369 | return errors.New("certdb: invalid bundle" + r.Bundle + " (valid bundles are ca|int)") |
| 370 | } |
| 371 | |
| 372 | // NewRelease verifies the bundle is valid, and creates a new Release |
| 373 | // with the current time stamp. |
| 374 | func NewRelease(bundle, version string) (*Release, error) { |
| 375 | r := &Release{ |
| 376 | Bundle: bundle, |
| 377 | Version: version, |
| 378 | ReleasedAt: time.Now().Unix(), |
| 379 | } |
| 380 | |
| 381 | if !r.validBundle() { |
| 382 | return nil, r.errInvalidBundle() |
| 383 | } |
| 384 | |
| 385 | return r, nil |
| 386 | } |
| 387 | |
| 388 | // Insert stores the Release in the database. |
| 389 | func (r *Release) Insert(tx *sql.Tx) error { |
| 390 | if !r.validBundle() { |
| 391 | return r.errInvalidBundle() |
| 392 | } |
| 393 | |
| 394 | query := fmt.Sprintf("INSERT INTO %s_releases (version, released_at) VALUES (?, ?)", |
| 395 | r.table()) |
| 396 | _, err := tx.Exec(query, r.Version, r.ReleasedAt) |
| 397 | return err |
| 398 | } |
| 399 | |
| 400 | // Select requires the Version field to have been populated. |
| 401 | func (r *Release) Select(tx *sql.Tx) error { |
| 402 | if !r.validBundle() { |
| 403 | return r.errInvalidBundle() |
| 404 | } |
| 405 | |
| 406 | query := fmt.Sprintf("SELECT released_at FROM %s_releases WHERE version=?", r.table()) |
| 407 | row := tx.QueryRow(query, r.Version) |
| 408 | return row.Scan(&r.ReleasedAt) |
| 409 | } |
| 410 | |
| 411 | // Count requires the Release to be Selectable, and will return the |
| 412 | // number of certificates in the release. |
| 413 | func (r *Release) Count(db *sql.DB) (int, error) { |
| 414 | tx, err := db.Begin() |
| 415 | if err != nil { |
| 416 | return 0, err |
| 417 | } |
| 418 | defer tx.Rollback() |
| 419 | |
| 420 | err = r.Select(tx) |
| 421 | if err != nil { |
| 422 | if err == sql.ErrNoRows { |
| 423 | err = errors.New("model/certdb: release doesn't exist") |
| 424 | } |
| 425 | return 0, err |
| 426 | } |
| 427 | |
| 428 | var count int |
| 429 | q := fmt.Sprintf("SELECT count(*) FROM %ss WHERE release = ?", r.table()) |
| 430 | row := tx.QueryRow(q, r.Version) |
| 431 | err = row.Scan(&count) |
| 432 | if err == nil { |
| 433 | err = tx.Commit() |
| 434 | } |
| 435 | return count, err |
| 436 | } |
| 437 | |
| 438 | // Previous returns the previous release. The Release must fully filled out. |
| 439 | func (r *Release) Previous(db *sql.DB) (*Release, error) { |
| 440 | tx, err := db.Begin() |
| 441 | if err != nil { |
| 442 | return nil, err |
| 443 | } |
| 444 | defer tx.Rollback() |
| 445 | |
| 446 | var prev = &Release{Bundle: r.Bundle} |
| 447 | query := fmt.Sprintf(`SELECT version, released_at FROM %s_releases WHERE released_at < ? ORDER BY released_at DESC LIMIT 1`, r.table()) |
| 448 | row := tx.QueryRow(query, r.ReleasedAt) |
| 449 | err = row.Scan(&prev.Version, &prev.ReleasedAt) |
| 450 | if err == nil { |
| 451 | err = tx.Commit() |
| 452 | } |
| 453 | return prev, err |
| 454 | } |
| 455 | |
| 456 | // AllReleases returns the list of all releases, sorted in reverse chronological |
| 457 | // order. |
| 458 | func AllReleases(db *sql.DB, bundle string) ([]*Release, error) { |
| 459 | if !validBundle(bundle) { |
| 460 | return nil, errors.New("model/certdb: invalid bundle " + bundle) |
| 461 | } |
| 462 | |
| 463 | tx, err := db.Begin() |
| 464 | if err != nil { |
| 465 | return nil, err |
| 466 | } |
| 467 | defer tx.Rollback() // nop if commit is called. |
| 468 | |
| 469 | tbl := tableForBundle(bundle) |
| 470 | var releases []*Release |
| 471 | |
| 472 | rows, err := tx.Query(fmt.Sprintf(`SELECT version,released_at FROM %s_releases ORDER BY released_at DESC`, tbl)) |
| 473 | if err != nil { |
| 474 | return nil, err |
| 475 | } |
| 476 | |
| 477 | for rows.Next() { |
| 478 | release := &Release{Bundle: bundle} |
| 479 | err = rows.Scan(&release.Version, &release.ReleasedAt) |
| 480 | if err != nil { |
| 481 | break |
| 482 | } |
| 483 | releases = append(releases, release) |
| 484 | } |
| 485 | |
| 486 | if err == nil { |
| 487 | err = tx.Commit() |
| 488 | } |
| 489 | |
| 490 | return releases, err |
| 491 | } |
| 492 | |
| 493 | // LatestRelease returns the latest release. |
| 494 | func LatestRelease(db *sql.DB, bundle string) (*Release, error) { |
| 495 | if !validBundle(bundle) { |
| 496 | return nil, errors.New("model/certdb: invalid bundle " + bundle) |
| 497 | } |
| 498 | |
| 499 | tx, err := db.Begin() |
| 500 | if err != nil { |
| 501 | return nil, err |
| 502 | } |
| 503 | defer tx.Rollback() // nop if commit is called. |
| 504 | |
| 505 | release := &Release{Bundle: bundle} |
| 506 | tbl := tableForBundle(bundle) |
| 507 | q := fmt.Sprintf(`SELECT version,released_at FROM %s_releases ORDER BY released_at DESC LIMIT 1`, tbl) |
| 508 | |
| 509 | row := tx.QueryRow(q) |
| 510 | err = row.Scan(&release.Version, &release.ReleasedAt) |
| 511 | if err == nil { |
| 512 | err = tx.Commit() |
| 513 | } |
| 514 | |
| 515 | return release, err |
| 516 | } |
| 517 | |
| 518 | // FetchRelease looks for the specified release. It does its own |
| 519 | // transaction to match the style of the other release fetching |
| 520 | // functions. |
| 521 | func FetchRelease(db *sql.DB, bundle, version string) (*Release, error) { |
| 522 | rel, err := NewRelease(bundle, version) |
| 523 | if err != nil { |
| 524 | return nil, err |
| 525 | } |
| 526 | |
| 527 | tx, err := db.Begin() |
| 528 | if err != nil { |
| 529 | return nil, err |
| 530 | } |
| 531 | defer tx.Rollback() // nop if commit is called. |
| 532 | |
| 533 | err = rel.Select(tx) |
| 534 | if err != nil { |
| 535 | return nil, err |
| 536 | } |
| 537 | |
| 538 | err = tx.Commit() |
| 539 | if err != nil { |
| 540 | return nil, err |
| 541 | } |
| 542 | |
| 543 | return rel, nil |
| 544 | } |
| 545 | |
| 546 | // A CertificateRelease pairs a Certificate and Release to enable adding |
| 547 | // certificates to the relevant release tables. |
| 548 | type CertificateRelease struct { |
| 549 | Certificate *Certificate |
| 550 | Release *Release |
| 551 | } |
| 552 | |
| 553 | // NewCertificateRelease is a convenience function for building a |
| 554 | // CertificateRelease structure. |
| 555 | func NewCertificateRelease(c *Certificate, r *Release) *CertificateRelease { |
| 556 | return &CertificateRelease{ |
| 557 | Certificate: c, |
| 558 | Release: r, |
| 559 | } |
| 560 | } |
| 561 | |
| 562 | // Insert stores the CertificateRelease in the database. It does no |
| 563 | // checking to determine if the CertificateRelease is already in the |
| 564 | // database, and will fail if it's already present in the database |
| 565 | // (due to UNIQUE constraints). |
| 566 | func (cr *CertificateRelease) Insert(tx *sql.Tx) error { |
| 567 | query := fmt.Sprintf("INSERT INTO %ss (ski, serial, release) VALUES (?, ?, ?)", cr.Release.table()) |
| 568 | _, err := tx.Exec(query, cr.Certificate.SKI, cr.Certificate.Serial, cr.Release.Version) |
| 569 | return err |
| 570 | } |
| 571 | |
| 572 | // Select requires the Certificate field to have the SKI and Serial |
| 573 | // filled in, and the Release field to have the Version field filled |
| 574 | // in. |
| 575 | func (cr *CertificateRelease) Select(tx *sql.Tx) error { |
| 576 | var count int |
| 577 | query := fmt.Sprintf("SELECT count(*) FROM %ss WHERE ski=? AND serial=? AND release=?", cr.Release.table()) |
| 578 | row := tx.QueryRow(query, cr.Certificate.SKI, cr.Certificate.Serial, cr.Release.Version) |
| 579 | err := row.Scan(&count) |
| 580 | if err == nil && count == 0 { |
| 581 | return sql.ErrNoRows |
| 582 | } |
| 583 | return err |
| 584 | } |
| 585 | |
| 586 | // Revocation models the revocations table. |
| 587 | type Revocation struct { |
| 588 | SKI string |
| 589 | RevokedAt int64 |
| 590 | Mechanism string |
| 591 | Reason string |
| 592 | } |
| 593 | |
| 594 | // Select requires the SKI field to be filled in. Note that only one |
| 595 | // revocation per SKI should exist. |
| 596 | func (rev *Revocation) Select(tx *sql.Tx) error { |
| 597 | row := tx.QueryRow(`SELECT revoked_at, mechanism, reason FROM revocations WHERE ski=?`, rev.SKI) |
| 598 | err := row.Scan(&rev.RevokedAt, &rev.Mechanism, &rev.Reason) |
| 599 | if err != nil { |
| 600 | return err |
| 601 | } |
| 602 | |
| 603 | return nil |
| 604 | } |
| 605 | |
| 606 | // Insert adds the revocation to the database if no revocation exists |
| 607 | // yet. |
| 608 | func (rev *Revocation) Insert(tx *sql.Tx) error { |
| 609 | _, err := tx.Exec(`INSERT INTO revocations (ski, revoked_at, mechanism, reason) VALUES (?, ?, ?, ?)`, rev.SKI, rev.RevokedAt, rev.Mechanism, rev.Reason) |
| 610 | return err |
| 611 | } |
| 612 | |