github.com/letsencrypt/boulder@v0.20251208.0/sa/model.go (about) 1 package sa 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "crypto/x509" 7 "database/sql" 8 "encoding/base64" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "google.golang.org/protobuf/proto" 13 "math" 14 "net/netip" 15 "net/url" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/go-jose/go-jose/v4" 21 "google.golang.org/protobuf/types/known/durationpb" 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "github.com/letsencrypt/boulder/core" 25 corepb "github.com/letsencrypt/boulder/core/proto" 26 "github.com/letsencrypt/boulder/db" 27 berrors "github.com/letsencrypt/boulder/errors" 28 "github.com/letsencrypt/boulder/grpc" 29 "github.com/letsencrypt/boulder/identifier" 30 "github.com/letsencrypt/boulder/probs" 31 "github.com/letsencrypt/boulder/revocation" 32 sapb "github.com/letsencrypt/boulder/sa/proto" 33 ) 34 35 // errBadJSON is an error type returned when a json.Unmarshal performed by the 36 // SA fails. It includes both the Unmarshal error and the original JSON data in 37 // its error message to make it easier to track down the bad JSON data. 38 type errBadJSON struct { 39 msg string 40 json []byte 41 err error 42 } 43 44 // Error returns an error message that includes the json.Unmarshal error as well 45 // as the bad JSON data. 46 func (e errBadJSON) Error() string { 47 return fmt.Sprintf( 48 "%s: error unmarshaling JSON %q: %s", 49 e.msg, 50 string(e.json), 51 e.err) 52 } 53 54 // badJSONError is a convenience function for constructing a errBadJSON instance 55 // with the provided args. 56 func badJSONError(msg string, jsonData []byte, err error) error { 57 return errBadJSON{ 58 msg: msg, 59 json: jsonData, 60 err: err, 61 } 62 } 63 64 const regFields = "id, jwk, jwk_sha256, agreement, createdAt, status" 65 66 // selectRegistration selects all fields of one registration model 67 func selectRegistration(ctx context.Context, s db.OneSelector, whereCol string, args ...any) (*regModel, error) { 68 if whereCol != "id" && whereCol != "jwk_sha256" { 69 return nil, fmt.Errorf("column name %q invalid for registrations table WHERE clause", whereCol) 70 } 71 72 var model regModel 73 err := s.SelectOne( 74 ctx, 75 &model, 76 "SELECT "+regFields+" FROM registrations WHERE "+whereCol+" = ? LIMIT 1", 77 args..., 78 ) 79 return &model, err 80 } 81 82 const certFields = "id, registrationID, serial, digest, der, issued, expires" 83 84 // SelectCertificate selects all fields of one certificate object identified by 85 // a serial. If more than one row contains the same serial only the first is 86 // returned. 87 func SelectCertificate(ctx context.Context, s db.OneSelector, serial string) (*corepb.Certificate, error) { 88 var model certificateModel 89 err := s.SelectOne( 90 ctx, 91 &model, 92 "SELECT "+certFields+" FROM certificates WHERE serial = ? LIMIT 1", 93 serial, 94 ) 95 return model.toPb(), err 96 } 97 98 const precertFields = "registrationID, serial, der, issued, expires" 99 100 // SelectPrecertificate selects all fields of one precertificate object 101 // identified by serial. 102 func SelectPrecertificate(ctx context.Context, s db.OneSelector, serial string) (*corepb.Certificate, error) { 103 var model lintingCertModel 104 err := s.SelectOne( 105 ctx, 106 &model, 107 "SELECT "+precertFields+" FROM precertificates WHERE serial = ? LIMIT 1", 108 serial) 109 if err != nil { 110 return nil, err 111 } 112 return model.toPb(), nil 113 } 114 115 // SelectCertificates selects all fields of multiple certificate objects 116 // 117 // Returns a slice of *corepb.Certificate along with the highest ID field seen 118 // (which can be used as input to a subsequent query when iterating in primary 119 // key order). 120 func SelectCertificates(ctx context.Context, s db.Selector, q string, args map[string]any) ([]*corepb.Certificate, int64, error) { 121 var models []certificateModel 122 _, err := s.Select( 123 ctx, 124 &models, 125 "SELECT "+certFields+" FROM certificates "+q, args) 126 var pbs []*corepb.Certificate 127 var highestID int64 128 for _, m := range models { 129 pbs = append(pbs, m.toPb()) 130 if m.ID > highestID { 131 highestID = m.ID 132 } 133 } 134 return pbs, highestID, err 135 } 136 137 type CertStatusMetadata struct { 138 ID int64 `db:"id"` 139 Serial string `db:"serial"` 140 Status core.OCSPStatus `db:"status"` 141 OCSPLastUpdated time.Time `db:"ocspLastUpdated"` 142 RevokedDate time.Time `db:"revokedDate"` 143 RevokedReason revocation.Reason `db:"revokedReason"` 144 LastExpirationNagSent time.Time `db:"lastExpirationNagSent"` 145 NotAfter time.Time `db:"notAfter"` 146 IsExpired bool `db:"isExpired"` 147 IssuerID int64 `db:"issuerID"` 148 } 149 150 const certStatusFields = "id, serial, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent, notAfter, isExpired, issuerID" 151 152 // SelectCertificateStatus selects all fields of one certificate status model 153 // identified by serial 154 func SelectCertificateStatus(ctx context.Context, s db.OneSelector, serial string) (*corepb.CertificateStatus, error) { 155 var model certificateStatusModel 156 err := s.SelectOne( 157 ctx, 158 &model, 159 "SELECT "+certStatusFields+" FROM certificateStatus WHERE serial = ? LIMIT 1", 160 serial, 161 ) 162 return model.toPb(), err 163 } 164 165 // RevocationStatusModel represents a small subset of the columns in the 166 // certificateStatus table, used to determine the authoritative revocation 167 // status of a certificate. 168 type RevocationStatusModel struct { 169 Status core.OCSPStatus `db:"status"` 170 RevokedDate time.Time `db:"revokedDate"` 171 RevokedReason revocation.Reason `db:"revokedReason"` 172 } 173 174 // SelectRevocationStatus returns the authoritative revocation information for 175 // the certificate with the given serial. 176 func SelectRevocationStatus(ctx context.Context, s db.OneSelector, serial string) (*sapb.RevocationStatus, error) { 177 var model RevocationStatusModel 178 err := s.SelectOne( 179 ctx, 180 &model, 181 "SELECT status, revokedDate, revokedReason FROM certificateStatus WHERE serial = ? LIMIT 1", 182 serial, 183 ) 184 if err != nil { 185 return nil, err 186 } 187 188 statusInt, ok := core.OCSPStatusToInt[model.Status] 189 if !ok { 190 return nil, fmt.Errorf("got unrecognized status %q", model.Status) 191 } 192 193 return &sapb.RevocationStatus{ 194 Status: int64(statusInt), 195 RevokedDate: timestamppb.New(model.RevokedDate), 196 RevokedReason: int64(model.RevokedReason), 197 }, nil 198 } 199 200 var mediumBlobSize = int(math.Pow(2, 24)) 201 202 type issuedNameModel struct { 203 ID int64 `db:"id"` 204 ReversedName string `db:"reversedName"` 205 NotBefore time.Time `db:"notBefore"` 206 Serial string `db:"serial"` 207 } 208 209 // regModel is the description of a core.Registration in the database before 210 type regModel struct { 211 ID int64 `db:"id"` 212 Key []byte `db:"jwk"` 213 KeySHA256 string `db:"jwk_sha256"` 214 Agreement string `db:"agreement"` 215 CreatedAt time.Time `db:"createdAt"` 216 Status string `db:"status"` 217 } 218 219 func registrationPbToModel(reg *corepb.Registration) (*regModel, error) { 220 // Even though we don't need to convert from JSON to an in-memory JSONWebKey 221 // for the sake of the `Key` field, we do need to do the conversion in order 222 // to compute the SHA256 key digest. 223 var jwk jose.JSONWebKey 224 err := jwk.UnmarshalJSON(reg.Key) 225 if err != nil { 226 return nil, err 227 } 228 sha, err := core.KeyDigestB64(jwk.Key) 229 if err != nil { 230 return nil, err 231 } 232 233 var createdAt time.Time 234 if !core.IsAnyNilOrZero(reg.CreatedAt) { 235 createdAt = reg.CreatedAt.AsTime() 236 } 237 238 return ®Model{ 239 ID: reg.Id, 240 Key: reg.Key, 241 KeySHA256: sha, 242 Agreement: reg.Agreement, 243 CreatedAt: createdAt, 244 Status: reg.Status, 245 }, nil 246 } 247 248 func registrationModelToPb(reg *regModel) (*corepb.Registration, error) { 249 if reg.ID == 0 || len(reg.Key) == 0 { 250 return nil, errors.New("incomplete Registration retrieved from DB") 251 } 252 253 return &corepb.Registration{ 254 Id: reg.ID, 255 Key: reg.Key, 256 Agreement: reg.Agreement, 257 CreatedAt: timestamppb.New(reg.CreatedAt.UTC()), 258 Status: reg.Status, 259 }, nil 260 } 261 262 type recordedSerialModel struct { 263 ID int64 264 Serial string 265 RegistrationID int64 266 Created time.Time 267 Expires time.Time 268 } 269 270 type lintingCertModel struct { 271 ID int64 272 Serial string 273 RegistrationID int64 274 DER []byte 275 Issued time.Time 276 Expires time.Time 277 } 278 279 func (model lintingCertModel) toPb() *corepb.Certificate { 280 return &corepb.Certificate{ 281 RegistrationID: model.RegistrationID, 282 Serial: model.Serial, 283 Digest: "", 284 Der: model.DER, 285 Issued: timestamppb.New(model.Issued), 286 Expires: timestamppb.New(model.Expires), 287 } 288 } 289 290 type certificateModel struct { 291 ID int64 `db:"id"` 292 RegistrationID int64 `db:"registrationID"` 293 Serial string `db:"serial"` 294 Digest string `db:"digest"` 295 DER []byte `db:"der"` 296 Issued time.Time `db:"issued"` 297 Expires time.Time `db:"expires"` 298 } 299 300 func (model certificateModel) toPb() *corepb.Certificate { 301 return &corepb.Certificate{ 302 RegistrationID: model.RegistrationID, 303 Serial: model.Serial, 304 Digest: model.Digest, 305 Der: model.DER, 306 Issued: timestamppb.New(model.Issued), 307 Expires: timestamppb.New(model.Expires), 308 } 309 } 310 311 type certificateStatusModel struct { 312 ID int64 `db:"id"` 313 Serial string `db:"serial"` 314 Status core.OCSPStatus `db:"status"` 315 OCSPLastUpdated time.Time `db:"ocspLastUpdated"` 316 RevokedDate time.Time `db:"revokedDate"` 317 RevokedReason revocation.Reason `db:"revokedReason"` 318 LastExpirationNagSent time.Time `db:"lastExpirationNagSent"` 319 NotAfter time.Time `db:"notAfter"` 320 IsExpired bool `db:"isExpired"` 321 IssuerID int64 `db:"issuerID"` 322 } 323 324 func (model certificateStatusModel) toPb() *corepb.CertificateStatus { 325 return &corepb.CertificateStatus{ 326 Serial: model.Serial, 327 Status: string(model.Status), 328 OcspLastUpdated: timestamppb.New(model.OCSPLastUpdated), 329 RevokedDate: timestamppb.New(model.RevokedDate), 330 RevokedReason: int64(model.RevokedReason), 331 LastExpirationNagSent: timestamppb.New(model.LastExpirationNagSent), 332 NotAfter: timestamppb.New(model.NotAfter), 333 IsExpired: model.IsExpired, 334 IssuerID: model.IssuerID, 335 } 336 } 337 338 // orderModel represents one row in the orders table. The CertificateProfileName 339 // column is a pointer because the column is NULL-able. 340 type orderModel struct { 341 ID int64 342 RegistrationID int64 343 Expires time.Time 344 Created time.Time 345 Error []byte 346 CertificateSerial string 347 BeganProcessing bool 348 CertificateProfileName *string 349 Replaces *string 350 Authzs []byte 351 } 352 353 type orderToAuthzModel struct { 354 OrderID int64 355 AuthzID int64 356 } 357 358 func modelToOrder(om *orderModel) (*corepb.Order, error) { 359 profile := "" 360 if om.CertificateProfileName != nil { 361 profile = *om.CertificateProfileName 362 } 363 replaces := "" 364 if om.Replaces != nil { 365 replaces = *om.Replaces 366 } 367 var v2Authorizations []int64 368 if len(om.Authzs) > 0 { 369 var decodedAuthzs sapb.Authzs 370 err := proto.Unmarshal(om.Authzs, &decodedAuthzs) 371 if err != nil { 372 return nil, err 373 } 374 v2Authorizations = decodedAuthzs.AuthzIDs 375 } 376 order := &corepb.Order{ 377 Id: om.ID, 378 RegistrationID: om.RegistrationID, 379 Expires: timestamppb.New(om.Expires), 380 Created: timestamppb.New(om.Created), 381 CertificateSerial: om.CertificateSerial, 382 BeganProcessing: om.BeganProcessing, 383 CertificateProfileName: profile, 384 Replaces: replaces, 385 V2Authorizations: v2Authorizations, 386 } 387 if len(om.Error) > 0 { 388 var problem corepb.ProblemDetails 389 err := json.Unmarshal(om.Error, &problem) 390 if err != nil { 391 return &corepb.Order{}, badJSONError( 392 "failed to unmarshal order model's error", 393 om.Error, 394 err) 395 } 396 order.Error = &problem 397 } 398 return order, nil 399 } 400 401 var challTypeToUint = map[string]uint8{ 402 "http-01": 0, 403 "dns-01": 1, 404 "tls-alpn-01": 2, 405 "dns-account-01": 3, 406 } 407 408 var uintToChallType = map[uint8]string{ 409 0: "http-01", 410 1: "dns-01", 411 2: "tls-alpn-01", 412 3: "dns-account-01", 413 } 414 415 var identifierTypeToUint = map[string]uint8{ 416 "dns": 0, 417 "ip": 1, 418 } 419 420 var uintToIdentifierType = map[uint8]identifier.IdentifierType{ 421 0: "dns", 422 1: "ip", 423 } 424 425 var statusToUint = map[core.AcmeStatus]uint8{ 426 core.StatusPending: 0, 427 core.StatusValid: 1, 428 core.StatusInvalid: 2, 429 core.StatusDeactivated: 3, 430 core.StatusRevoked: 4, 431 } 432 433 var uintToStatus = map[uint8]core.AcmeStatus{ 434 0: core.StatusPending, 435 1: core.StatusValid, 436 2: core.StatusInvalid, 437 3: core.StatusDeactivated, 438 4: core.StatusRevoked, 439 } 440 441 func statusUint(status core.AcmeStatus) uint8 { 442 return statusToUint[status] 443 } 444 445 // authzFields is used in a variety of places in sa.go, and modifications to 446 // it must be carried through to every use in sa.go 447 const authzFields = "id, identifierType, identifierValue, registrationID, certificateProfileName, status, expires, challenges, attempted, attemptedAt, token, validationError, validationRecord" 448 449 // authzModel represents one row in the authz2 table. The CertificateProfileName 450 // column is a pointer because the column is NULL-able. 451 type authzModel struct { 452 ID int64 `db:"id"` 453 IdentifierType uint8 `db:"identifierType"` 454 IdentifierValue string `db:"identifierValue"` 455 RegistrationID int64 `db:"registrationID"` 456 CertificateProfileName *string `db:"certificateProfileName"` 457 Status uint8 `db:"status"` 458 Expires time.Time `db:"expires"` 459 Challenges uint8 `db:"challenges"` 460 Attempted *uint8 `db:"attempted"` 461 AttemptedAt *time.Time `db:"attemptedAt"` 462 Token []byte `db:"token"` 463 ValidationError []byte `db:"validationError"` 464 ValidationRecord []byte `db:"validationRecord"` 465 } 466 467 // rehydrateHostPort mutates a validation record. If the URL in the validation 468 // record cannot be parsed, an error will be returned. If the Hostname and Port 469 // fields already exist in the validation record, they will be retained. 470 // Otherwise, the Hostname and Port will be derived and set from the URL field 471 // of the validation record. 472 func rehydrateHostPort(vr *core.ValidationRecord) error { 473 if vr.URL == "" { 474 return fmt.Errorf("rehydrating validation record, URL field cannot be empty") 475 } 476 477 parsedUrl, err := url.Parse(vr.URL) 478 if err != nil { 479 return fmt.Errorf("parsing validation record URL %q: %w", vr.URL, err) 480 } 481 482 if vr.Hostname == "" { 483 hostname := parsedUrl.Hostname() 484 if hostname == "" { 485 return fmt.Errorf("hostname missing in URL %q", vr.URL) 486 } 487 vr.Hostname = hostname 488 } 489 490 if vr.Port == "" { 491 // CABF BRs section 1.6.1: Authorized Ports: One of the following ports: 80 492 // (http), 443 (https) 493 if parsedUrl.Port() == "" { 494 // If there is only a scheme, then we'll determine the appropriate port. 495 switch parsedUrl.Scheme { 496 case "https": 497 vr.Port = "443" 498 case "http": 499 vr.Port = "80" 500 default: 501 // This should never happen since the VA should have already 502 // checked the scheme. 503 return fmt.Errorf("unknown scheme %q in URL %q", parsedUrl.Scheme, vr.URL) 504 } 505 } else if parsedUrl.Port() == "80" || parsedUrl.Port() == "443" { 506 // If :80 or :443 were embedded in the URL field 507 // e.g. '"url":"https://example.com:443"' 508 vr.Port = parsedUrl.Port() 509 } else { 510 return fmt.Errorf("only ports 80/tcp and 443/tcp are allowed in URL %q", vr.URL) 511 } 512 } 513 514 return nil 515 } 516 517 // SelectAuthzsMatchingIssuance looks for a set of authzs that would have 518 // authorized a given issuance that is known to have occurred. The returned 519 // authzs will all belong to the given regID, will have potentially been valid 520 // at the time of issuance, and will have the appropriate identifier type and 521 // value. This may return multiple authzs for the same identifier type and value. 522 // 523 // This returns "potentially" valid authzs because a client may have set an 524 // authzs status to deactivated after issuance, so we return both valid and 525 // deactivated authzs. It also uses a small amount of leeway (1s) to account 526 // for possible clock skew. 527 // 528 // This function doesn't do anything special for authzs with an expiration in 529 // the past. If the stored authz has a valid status, it is returned with a 530 // valid status regardless of whether it is also expired. 531 func SelectAuthzsMatchingIssuance( 532 ctx context.Context, 533 s db.Selector, 534 regID int64, 535 issued time.Time, 536 idents identifier.ACMEIdentifiers, 537 ) ([]*corepb.Authorization, error) { 538 // The WHERE clause returned by this function does not contain any 539 // user-controlled strings; all user-controlled input ends up in the 540 // returned placeholder args. 541 identConditions, identArgs := buildIdentifierQueryConditions(idents) 542 query := fmt.Sprintf(`SELECT %s FROM authz2 WHERE 543 registrationID = ? AND 544 status IN (?, ?) AND 545 expires >= ? AND 546 attemptedAt <= ? AND 547 (%s)`, 548 authzFields, 549 identConditions) 550 var args []any 551 args = append(args, 552 regID, 553 statusToUint[core.StatusValid], statusToUint[core.StatusDeactivated], 554 issued.Add(-1*time.Second), // leeway for clock skew 555 issued.Add(1*time.Second), // leeway for clock skew 556 ) 557 args = append(args, identArgs...) 558 559 var authzModels []authzModel 560 _, err := s.Select(ctx, &authzModels, query, args...) 561 if err != nil { 562 return nil, err 563 } 564 565 var authzs []*corepb.Authorization 566 for _, model := range authzModels { 567 authz, err := modelToAuthzPB(model) 568 if err != nil { 569 return nil, err 570 } 571 authzs = append(authzs, authz) 572 573 } 574 return authzs, err 575 } 576 577 // hasMultipleNonPendingChallenges checks if a slice of challenges contains 578 // more than one non-pending challenge 579 func hasMultipleNonPendingChallenges(challenges []*corepb.Challenge) bool { 580 nonPending := false 581 for _, c := range challenges { 582 if c.Status == string(core.StatusValid) || c.Status == string(core.StatusInvalid) { 583 if !nonPending { 584 nonPending = true 585 } else { 586 return true 587 } 588 } 589 } 590 return false 591 } 592 593 // newAuthzReqToModel converts an sapb.NewAuthzRequest to the authzModel storage 594 // representation. It hardcodes the status to "pending" because it should be 595 // impossible to create an authz in any other state. 596 func newAuthzReqToModel(authz *sapb.NewAuthzRequest, profile string) (*authzModel, error) { 597 am := &authzModel{ 598 IdentifierType: identifierTypeToUint[authz.Identifier.Type], 599 IdentifierValue: authz.Identifier.Value, 600 RegistrationID: authz.RegistrationID, 601 Status: statusToUint[core.StatusPending], 602 Expires: authz.Expires.AsTime(), 603 } 604 605 if profile != "" { 606 am.CertificateProfileName = &profile 607 } 608 609 for _, challType := range authz.ChallengeTypes { 610 // Set the challenge type bit in the bitmap 611 am.Challenges |= 1 << challTypeToUint[challType] 612 } 613 614 token, err := base64.RawURLEncoding.DecodeString(authz.Token) 615 if err != nil { 616 return nil, err 617 } 618 am.Token = token 619 620 return am, nil 621 } 622 623 // authzPBToModel converts a protobuf authorization representation to the 624 // authzModel storage representation. 625 // Deprecated: this function is only used as part of test setup, do not 626 // introduce any new uses in production code. 627 func authzPBToModel(authz *corepb.Authorization) (*authzModel, error) { 628 ident := identifier.FromProto(authz.Identifier) 629 630 am := &authzModel{ 631 IdentifierType: identifierTypeToUint[ident.ToProto().Type], 632 IdentifierValue: ident.Value, 633 RegistrationID: authz.RegistrationID, 634 Status: statusToUint[core.AcmeStatus(authz.Status)], 635 Expires: authz.Expires.AsTime(), 636 } 637 if authz.CertificateProfileName != "" { 638 profile := authz.CertificateProfileName 639 am.CertificateProfileName = &profile 640 } 641 if authz.Id != "" { 642 // The v1 internal authorization objects use a string for the ID, the v2 643 // storage format uses a integer ID. In order to maintain compatibility we 644 // convert the integer ID to a string. 645 id, err := strconv.Atoi(authz.Id) 646 if err != nil { 647 return nil, err 648 } 649 am.ID = int64(id) 650 } 651 if hasMultipleNonPendingChallenges(authz.Challenges) { 652 return nil, errors.New("multiple challenges are non-pending") 653 } 654 // In the v2 authorization style we don't store individual challenges with their own 655 // token, validation errors/records, etc. Instead we store a single token/error/record 656 // set, a bitmap of available challenge types, and a row indicating which challenge type 657 // was 'attempted'. 658 // 659 // Since we don't currently have the singular token/error/record set abstracted out to 660 // the core authorization type yet we need to extract these from the challenges array. 661 // We assume that the token in each challenge is the same and that if any of the challenges 662 // has a non-pending status that it should be considered the 'attempted' challenge and 663 // we extract the error/record set from that particular challenge. 664 var tokenStr string 665 for _, chall := range authz.Challenges { 666 // Set the challenge type bit in the bitmap 667 am.Challenges |= 1 << challTypeToUint[chall.Type] 668 tokenStr = chall.Token 669 // If the challenge status is not core.StatusPending we assume it was the 'attempted' 670 // challenge and extract the relevant fields we need. 671 if chall.Status == string(core.StatusValid) || chall.Status == string(core.StatusInvalid) { 672 attemptedType := challTypeToUint[chall.Type] 673 am.Attempted = &attemptedType 674 675 // If validated Unix timestamp is zero then keep the core.Challenge Validated object nil. 676 var validated *time.Time 677 if !core.IsAnyNilOrZero(chall.Validated) { 678 val := chall.Validated.AsTime() 679 validated = &val 680 } 681 am.AttemptedAt = validated 682 683 // Marshal corepb.ValidationRecords to core.ValidationRecords so that we 684 // can marshal them to JSON. 685 records := make([]core.ValidationRecord, len(chall.Validationrecords)) 686 for i, recordPB := range chall.Validationrecords { 687 if chall.Type == string(core.ChallengeTypeHTTP01) { 688 // Remove these fields because they can be rehydrated later 689 // on from the URL field. 690 recordPB.Hostname = "" 691 recordPB.Port = "" 692 } 693 var err error 694 records[i], err = grpc.PBToValidationRecord(recordPB) 695 if err != nil { 696 return nil, err 697 } 698 } 699 var err error 700 am.ValidationRecord, err = json.Marshal(records) 701 if err != nil { 702 return nil, err 703 } 704 // If there is a error associated with the challenge marshal it to JSON 705 // so that we can store it in the database. 706 if chall.Error != nil { 707 prob, err := grpc.PBToProblemDetails(chall.Error) 708 if err != nil { 709 return nil, err 710 } 711 am.ValidationError, err = json.Marshal(prob) 712 if err != nil { 713 return nil, err 714 } 715 } 716 } 717 token, err := base64.RawURLEncoding.DecodeString(tokenStr) 718 if err != nil { 719 return nil, err 720 } 721 am.Token = token 722 } 723 724 return am, nil 725 } 726 727 // populateAttemptedFields takes a challenge and populates it with the validation fields status, 728 // validation records, and error (the latter only if the validation failed) from an authzModel. 729 func populateAttemptedFields(am authzModel, challenge *corepb.Challenge) error { 730 if len(am.ValidationError) != 0 { 731 // If the error is non-empty the challenge must be invalid. 732 challenge.Status = string(core.StatusInvalid) 733 var prob probs.ProblemDetails 734 err := json.Unmarshal(am.ValidationError, &prob) 735 if err != nil { 736 return badJSONError( 737 "failed to unmarshal authz2 model's validation error", 738 am.ValidationError, 739 err) 740 } 741 challenge.Error, err = grpc.ProblemDetailsToPB(&prob) 742 if err != nil { 743 return err 744 } 745 } else { 746 // If the error is empty the challenge must be valid. 747 challenge.Status = string(core.StatusValid) 748 } 749 var records []core.ValidationRecord 750 err := json.Unmarshal(am.ValidationRecord, &records) 751 if err != nil { 752 return badJSONError( 753 "failed to unmarshal authz2 model's validation record", 754 am.ValidationRecord, 755 err) 756 } 757 challenge.Validationrecords = make([]*corepb.ValidationRecord, len(records)) 758 for i, r := range records { 759 // Fixes implicit memory aliasing in for loop so we can deference r 760 // later on for rehydrateHostPort. 761 if challenge.Type == string(core.ChallengeTypeHTTP01) { 762 err := rehydrateHostPort(&r) 763 if err != nil { 764 return err 765 } 766 } 767 challenge.Validationrecords[i], err = grpc.ValidationRecordToPB(r) 768 if err != nil { 769 return err 770 } 771 } 772 return nil 773 } 774 775 func modelToAuthzPB(am authzModel) (*corepb.Authorization, error) { 776 identType, ok := uintToIdentifierType[am.IdentifierType] 777 if !ok { 778 return nil, fmt.Errorf("unrecognized identifier type encoding %d", am.IdentifierType) 779 } 780 781 profile := "" 782 if am.CertificateProfileName != nil { 783 profile = *am.CertificateProfileName 784 } 785 786 pb := &corepb.Authorization{ 787 Id: fmt.Sprintf("%d", am.ID), 788 Status: string(uintToStatus[am.Status]), 789 Identifier: identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue}.ToProto(), 790 RegistrationID: am.RegistrationID, 791 Expires: timestamppb.New(am.Expires), 792 CertificateProfileName: profile, 793 } 794 // Populate authorization challenge array. We do this by iterating through 795 // the challenge type bitmap and creating a challenge of each type if its 796 // bit is set. Each of these challenges has the token from the authorization 797 // model and has its status set to core.StatusPending by default. If the 798 // challenge type is equal to that in the 'attempted' row we set the status 799 // to core.StatusValid or core.StatusInvalid depending on if there is anything 800 // in ValidationError and populate the ValidationRecord and ValidationError 801 // fields. 802 for pos := range uint8(8) { 803 if (am.Challenges>>pos)&1 == 1 { 804 challType := uintToChallType[pos] 805 challenge := &corepb.Challenge{ 806 Type: challType, 807 Status: string(core.StatusPending), 808 Token: base64.RawURLEncoding.EncodeToString(am.Token), 809 } 810 // If the challenge type matches the attempted type it must be either 811 // valid or invalid and we need to populate extra fields. 812 // Also, once any challenge has been attempted, we consider the other 813 // challenges "gone" per https://tools.ietf.org/html/rfc8555#section-7.1.4 814 if am.Attempted != nil { 815 if uintToChallType[*am.Attempted] == challType { 816 err := populateAttemptedFields(am, challenge) 817 if err != nil { 818 return nil, err 819 } 820 // Get the attemptedAt time and assign to the challenge validated time. 821 var validated *timestamppb.Timestamp 822 if am.AttemptedAt != nil { 823 validated = timestamppb.New(*am.AttemptedAt) 824 } 825 challenge.Validated = validated 826 pb.Challenges = append(pb.Challenges, challenge) 827 } 828 } else { 829 // When no challenge has been attempted yet, all challenges are still 830 // present. 831 pb.Challenges = append(pb.Challenges, challenge) 832 } 833 } 834 } 835 return pb, nil 836 } 837 838 type keyHashModel struct { 839 ID int64 840 KeyHash []byte 841 CertNotAfter time.Time 842 CertSerial string 843 } 844 845 var stringToSourceInt = map[string]int{ 846 "API": 1, 847 "admin-revoker": 2, 848 } 849 850 // incidentModel represents a row in the 'incidents' table. 851 type incidentModel struct { 852 ID int64 `db:"id"` 853 SerialTable string `db:"serialTable"` 854 URL string `db:"url"` 855 RenewBy time.Time `db:"renewBy"` 856 Enabled bool `db:"enabled"` 857 } 858 859 func incidentModelToPB(i incidentModel) sapb.Incident { 860 return sapb.Incident{ 861 Id: i.ID, 862 SerialTable: i.SerialTable, 863 Url: i.URL, 864 RenewBy: timestamppb.New(i.RenewBy), 865 Enabled: i.Enabled, 866 } 867 } 868 869 // incidentSerialModel represents a row in an 'incident_*' table. 870 type incidentSerialModel struct { 871 Serial string `db:"serial"` 872 RegistrationID *int64 `db:"registrationID"` 873 OrderID *int64 `db:"orderID"` 874 LastNoticeSent *time.Time `db:"lastNoticeSent"` 875 } 876 877 // crlEntryModel has just the certificate status fields necessary to construct 878 // an entry in a CRL. 879 type crlEntryModel struct { 880 Serial string `db:"serial"` 881 Status core.OCSPStatus `db:"status"` 882 RevokedReason revocation.Reason `db:"revokedReason"` 883 RevokedDate time.Time `db:"revokedDate"` 884 } 885 886 // fqdnSet contains the SHA256 hash of the lowercased, comma joined dNSNames 887 // contained in a certificate. 888 type fqdnSet struct { 889 ID int64 890 SetHash []byte 891 Serial string 892 Issued time.Time 893 Expires time.Time 894 } 895 896 // orderFQDNSet contains the SHA256 hash of the lowercased, comma joined names 897 // from a new-order request, along with the corresponding orderID, the 898 // registration ID, and the order expiry. This is used to find 899 // existing orders for reuse. 900 type orderFQDNSet struct { 901 ID int64 902 SetHash []byte 903 OrderID int64 904 RegistrationID int64 905 Expires time.Time 906 } 907 908 func addFQDNSet(ctx context.Context, db db.Inserter, idents identifier.ACMEIdentifiers, serial string, issued time.Time, expires time.Time) error { 909 return db.Insert(ctx, &fqdnSet{ 910 SetHash: core.HashIdentifiers(idents), 911 Serial: serial, 912 Issued: issued, 913 Expires: expires, 914 }) 915 } 916 917 // addOrderFQDNSet creates a new OrderFQDNSet row using the provided 918 // information. This function accepts a transaction so that the orderFqdnSet 919 // addition can take place within the order addition transaction. The caller is 920 // required to rollback the transaction if an error is returned. 921 func addOrderFQDNSet( 922 ctx context.Context, 923 db db.Inserter, 924 idents identifier.ACMEIdentifiers, 925 orderID int64, 926 regID int64, 927 expires time.Time) error { 928 return db.Insert(ctx, &orderFQDNSet{ 929 SetHash: core.HashIdentifiers(idents), 930 OrderID: orderID, 931 RegistrationID: regID, 932 Expires: expires, 933 }) 934 } 935 936 // deleteOrderFQDNSet deletes a OrderFQDNSet row that matches the provided 937 // orderID. This function accepts a transaction so that the deletion can 938 // take place within the finalization transaction. The caller is required to 939 // rollback the transaction if an error is returned. 940 func deleteOrderFQDNSet( 941 ctx context.Context, 942 db db.Execer, 943 orderID int64) error { 944 945 result, err := db.ExecContext(ctx, ` 946 DELETE FROM orderFqdnSets 947 WHERE orderID = ?`, 948 orderID) 949 if err != nil { 950 return err 951 } 952 rowsDeleted, err := result.RowsAffected() 953 if err != nil { 954 return err 955 } 956 // We always expect there to be an order FQDN set row for each 957 // pending/processing order that is being finalized. If there isn't one then 958 // something is amiss and should be raised as an internal server error 959 if rowsDeleted == 0 { 960 return berrors.InternalServerError("No orderFQDNSet exists to delete") 961 } 962 return nil 963 } 964 965 func addIssuedNames(ctx context.Context, queryer db.Execer, cert *x509.Certificate, isRenewal bool) error { 966 if len(cert.DNSNames) == 0 && len(cert.IPAddresses) == 0 { 967 return berrors.InternalServerError("certificate has no DNSNames or IPAddresses") 968 } 969 970 multiInserter, err := db.NewMultiInserter("issuedNames", []string{"reversedName", "serial", "notBefore", "renewal"}) 971 if err != nil { 972 return err 973 } 974 for _, name := range cert.DNSNames { 975 err = multiInserter.Add([]any{ 976 reverseFQDN(name), 977 core.SerialToString(cert.SerialNumber), 978 cert.NotBefore.Truncate(24 * time.Hour), 979 isRenewal, 980 }) 981 if err != nil { 982 return err 983 } 984 } 985 for _, ip := range cert.IPAddresses { 986 err = multiInserter.Add([]any{ 987 ip.String(), 988 core.SerialToString(cert.SerialNumber), 989 cert.NotBefore.Truncate(24 * time.Hour), 990 isRenewal, 991 }) 992 if err != nil { 993 return err 994 } 995 } 996 return multiInserter.Insert(ctx, queryer) 997 } 998 999 // EncodeIssuedName translates a FQDN to/from the issuedNames table by reversing 1000 // its dot-separated elements, and translates an IP address by returning its 1001 // normal string form. 1002 // 1003 // This is for strings of ambiguous identifier values. If you know your string 1004 // is a FQDN, use reverseFQDN(). If you have an IP address, use 1005 // netip.Addr.String() or net.IP.String(). 1006 func EncodeIssuedName(name string) string { 1007 netIP, err := netip.ParseAddr(name) 1008 if err == nil { 1009 return netIP.String() 1010 } 1011 return reverseFQDN(name) 1012 } 1013 1014 // reverseFQDN reverses the elements of a dot-separated FQDN. 1015 // 1016 // If your string might be an IP address, use EncodeIssuedName() instead. 1017 func reverseFQDN(fqdn string) string { 1018 labels := strings.Split(fqdn, ".") 1019 for i, j := 0, len(labels)-1; i < j; i, j = i+1, j-1 { 1020 labels[i], labels[j] = labels[j], labels[i] 1021 } 1022 return strings.Join(labels, ".") 1023 } 1024 1025 func addKeyHash(ctx context.Context, db db.Inserter, cert *x509.Certificate) error { 1026 if cert.RawSubjectPublicKeyInfo == nil { 1027 return errors.New("certificate has a nil RawSubjectPublicKeyInfo") 1028 } 1029 h := sha256.Sum256(cert.RawSubjectPublicKeyInfo) 1030 khm := &keyHashModel{ 1031 KeyHash: h[:], 1032 CertNotAfter: cert.NotAfter, 1033 CertSerial: core.SerialToString(cert.SerialNumber), 1034 } 1035 return db.Insert(ctx, khm) 1036 } 1037 1038 var blockedKeysColumns = "keyHash, added, source, comment" 1039 1040 // statusForOrder examines the status of a provided order's authorizations to 1041 // determine what the overall status of the order should be. In summary: 1042 // - If the order has an error, the order is invalid 1043 // - If any of the order's authorizations are in any state other than 1044 // valid or pending, the order is invalid. 1045 // - If any of the order's authorizations are pending, the order is pending. 1046 // - If all of the order's authorizations are valid, and there is 1047 // a certificate serial, the order is valid. 1048 // - If all of the order's authorizations are valid, and we have began 1049 // processing, but there is no certificate serial, the order is processing. 1050 // - If all of the order's authorizations are valid, and we haven't begun 1051 // processing, then the order is status ready. 1052 // 1053 // An error is returned for any other case. 1054 func statusForOrder(order *corepb.Order, authzValidityInfo []authzValidity, now time.Time) (string, error) { 1055 // Without any further work we know an order with an error is invalid 1056 if order.Error != nil { 1057 return string(core.StatusInvalid), nil 1058 } 1059 1060 // If the order is expired the status is invalid and we don't need to get 1061 // order authorizations. Its important to exit early in this case because an 1062 // order that references an expired authorization will be itself have been 1063 // expired (because we match the order expiry to the associated authz expiries 1064 // in ra.NewOrder), and expired authorizations may be purged from the DB. 1065 // Because of this purging fetching the authz's for an expired order may 1066 // return fewer authz objects than expected, triggering a 500 error response. 1067 if order.Expires.AsTime().Before(now) { 1068 return string(core.StatusInvalid), nil 1069 } 1070 1071 // If getAuthorizationStatuses returned a different number of authorization 1072 // objects than the order's slice of authorization IDs something has gone 1073 // wrong worth raising an internal error about. 1074 if len(authzValidityInfo) != len(order.V2Authorizations) { 1075 return "", berrors.InternalServerError( 1076 "getAuthorizationStatuses returned the wrong number of authorization statuses "+ 1077 "(%d vs expected %d) for order %d", 1078 len(authzValidityInfo), len(order.V2Authorizations), order.Id) 1079 } 1080 1081 // Keep a count of the authorizations seen 1082 pendingAuthzs := 0 1083 validAuthzs := 0 1084 otherAuthzs := 0 1085 expiredAuthzs := 0 1086 1087 // Loop over each of the order's authorization objects to examine the authz status 1088 for _, info := range authzValidityInfo { 1089 switch uintToStatus[info.Status] { 1090 case core.StatusPending: 1091 pendingAuthzs++ 1092 case core.StatusValid: 1093 validAuthzs++ 1094 case core.StatusInvalid: 1095 otherAuthzs++ 1096 case core.StatusDeactivated: 1097 otherAuthzs++ 1098 case core.StatusRevoked: 1099 otherAuthzs++ 1100 default: 1101 return "", berrors.InternalServerError( 1102 "Order is in an invalid state. Authz has invalid status %d", 1103 info.Status) 1104 } 1105 if info.Expires.Before(now) { 1106 expiredAuthzs++ 1107 } 1108 } 1109 1110 // An order is invalid if **any** of its authzs are invalid, deactivated, 1111 // revoked, or expired, see https://tools.ietf.org/html/rfc8555#section-7.1.6 1112 if otherAuthzs > 0 || expiredAuthzs > 0 { 1113 return string(core.StatusInvalid), nil 1114 } 1115 // An order is pending if **any** of its authzs are pending 1116 if pendingAuthzs > 0 { 1117 return string(core.StatusPending), nil 1118 } 1119 1120 // An order is fully authorized if it has valid authzs for each of the order 1121 // identifiers 1122 fullyAuthorized := len(order.Identifiers) == validAuthzs 1123 1124 // If the order isn't fully authorized we've encountered an internal error: 1125 // Above we checked for any invalid or pending authzs and should have returned 1126 // early. Somehow we made it this far but also don't have the correct number 1127 // of valid authzs. 1128 if !fullyAuthorized { 1129 return "", berrors.InternalServerError( 1130 "Order has the incorrect number of valid authorizations & no pending, " + 1131 "deactivated or invalid authorizations") 1132 } 1133 1134 // If the order is fully authorized and the certificate serial is set then the 1135 // order is valid 1136 if fullyAuthorized && order.CertificateSerial != "" { 1137 return string(core.StatusValid), nil 1138 } 1139 1140 // If the order is fully authorized, and we have began processing it, then the 1141 // order is processing. 1142 if fullyAuthorized && order.BeganProcessing { 1143 return string(core.StatusProcessing), nil 1144 } 1145 1146 if fullyAuthorized && !order.BeganProcessing { 1147 return string(core.StatusReady), nil 1148 } 1149 1150 return "", berrors.InternalServerError( 1151 "Order %d is in an invalid state. No state known for this order's "+ 1152 "authorizations", order.Id) 1153 } 1154 1155 // authzValidity is a subset of authzModel 1156 type authzValidity struct { 1157 IdentifierType uint8 `db:"identifierType"` 1158 IdentifierValue string `db:"identifierValue"` 1159 Status uint8 `db:"status"` 1160 Expires time.Time `db:"expires"` 1161 } 1162 1163 // getAuthorizationStatuses takes a sequence of authz IDs, and returns the 1164 // status and expiration date of each of them. 1165 func getAuthorizationStatuses(ctx context.Context, s db.Selector, ids []int64) ([]authzValidity, error) { 1166 var params []any 1167 for _, id := range ids { 1168 params = append(params, id) 1169 } 1170 var validities []authzValidity 1171 _, err := s.Select( 1172 ctx, 1173 &validities, 1174 fmt.Sprintf("SELECT identifierType, identifierValue, status, expires FROM authz2 WHERE id IN (%s)", 1175 db.QuestionMarks(len(ids))), 1176 params..., 1177 ) 1178 if err != nil { 1179 return nil, err 1180 } 1181 1182 return validities, nil 1183 } 1184 1185 // authzForOrder retrieves the authorization IDs for an order. 1186 func authzForOrder(ctx context.Context, s db.Selector, orderID int64) ([]int64, error) { 1187 var v2IDs []int64 1188 _, err := s.Select( 1189 ctx, 1190 &v2IDs, 1191 "SELECT authzID FROM orderToAuthz2 WHERE orderID = ?", 1192 orderID, 1193 ) 1194 return v2IDs, err 1195 } 1196 1197 // crlShardModel represents one row in the crlShards table. The ThisUpdate and 1198 // NextUpdate fields are pointers because they are NULL-able columns. 1199 type crlShardModel struct { 1200 ID int64 `db:"id"` 1201 IssuerID int64 `db:"issuerID"` 1202 Idx int `db:"idx"` 1203 ThisUpdate *time.Time `db:"thisUpdate"` 1204 NextUpdate *time.Time `db:"nextUpdate"` 1205 LeasedUntil time.Time `db:"leasedUntil"` 1206 } 1207 1208 // revokedCertModel represents one row in the revokedCertificates table. It 1209 // contains all of the information necessary to populate a CRL entry or OCSP 1210 // response for the indicated certificate. 1211 type revokedCertModel struct { 1212 ID int64 `db:"id"` 1213 IssuerID int64 `db:"issuerID"` 1214 Serial string `db:"serial"` 1215 NotAfterHour time.Time `db:"notAfterHour"` 1216 ShardIdx int64 `db:"shardIdx"` 1217 RevokedDate time.Time `db:"revokedDate"` 1218 RevokedReason revocation.Reason `db:"revokedReason"` 1219 } 1220 1221 // replacementOrderModel represents one row in the replacementOrders table. It 1222 // contains all of the information necessary to link a renewal order to the 1223 // certificate it replaces. 1224 type replacementOrderModel struct { 1225 // ID is an auto-incrementing row ID. 1226 ID int64 `db:"id"` 1227 // Serial is the serial number of the replaced certificate. 1228 Serial string `db:"serial"` 1229 // OrderId is the ID of the replacement order 1230 OrderID int64 `db:"orderID"` 1231 // OrderExpiry is the expiry time of the new order. This is used to 1232 // determine if we can accept a new replacement order for the same Serial. 1233 OrderExpires time.Time `db:"orderExpires"` 1234 // Replaced is a boolean indicating whether the certificate has been 1235 // replaced, i.e. whether the new order has been finalized. Once this is 1236 // true, no new replacement orders can be accepted for the same Serial. 1237 Replaced bool `db:"replaced"` 1238 } 1239 1240 // addReplacementOrder inserts or updates the replacementOrders row matching the 1241 // provided serial with the details provided. This function accepts a 1242 // transaction so that the insert or update takes place within the new order 1243 // transaction. 1244 func addReplacementOrder(ctx context.Context, db db.SelectExecer, serial string, orderID int64, orderExpires time.Time) error { 1245 var existingID []int64 1246 _, err := db.Select(ctx, &existingID, ` 1247 SELECT id 1248 FROM replacementOrders 1249 WHERE serial = ? 1250 LIMIT 1`, 1251 serial, 1252 ) 1253 if err != nil && !errors.Is(err, sql.ErrNoRows) { 1254 return fmt.Errorf("checking for existing replacement order: %w", err) 1255 } 1256 1257 if len(existingID) > 0 { 1258 // Update existing replacementOrder row. 1259 _, err = db.ExecContext(ctx, ` 1260 UPDATE replacementOrders 1261 SET orderID = ?, orderExpires = ? 1262 WHERE id = ?`, 1263 orderID, orderExpires, 1264 existingID[0], 1265 ) 1266 if err != nil { 1267 return fmt.Errorf("updating replacement order: %w", err) 1268 } 1269 } else { 1270 // Insert new replacementOrder row. 1271 _, err = db.ExecContext(ctx, ` 1272 INSERT INTO replacementOrders (serial, orderID, orderExpires) 1273 VALUES (?, ?, ?)`, 1274 serial, orderID, orderExpires, 1275 ) 1276 if err != nil { 1277 return fmt.Errorf("creating replacement order: %w", err) 1278 } 1279 } 1280 return nil 1281 } 1282 1283 // setReplacementOrderFinalized sets the replaced flag for the replacementOrder 1284 // row matching the provided orderID to true. This function accepts a 1285 // transaction so that the update can take place within the finalization 1286 // transaction. 1287 func setReplacementOrderFinalized(ctx context.Context, db db.Execer, orderID int64) error { 1288 _, err := db.ExecContext(ctx, ` 1289 UPDATE replacementOrders 1290 SET replaced = true 1291 WHERE orderID = ? 1292 LIMIT 1`, 1293 orderID, 1294 ) 1295 if err != nil { 1296 return err 1297 } 1298 return nil 1299 } 1300 1301 type identifierModel struct { 1302 Type uint8 `db:"identifierType"` 1303 Value string `db:"identifierValue"` 1304 } 1305 1306 func newIdentifierModelFromPB(pb *corepb.Identifier) (identifierModel, error) { 1307 idType, ok := identifierTypeToUint[pb.Type] 1308 if !ok { 1309 return identifierModel{}, fmt.Errorf("unsupported identifier type %q", pb.Type) 1310 } 1311 1312 return identifierModel{ 1313 Type: idType, 1314 Value: pb.Value, 1315 }, nil 1316 } 1317 1318 func newPBFromIdentifierModel(id identifierModel) (*corepb.Identifier, error) { 1319 idType, ok := uintToIdentifierType[id.Type] 1320 if !ok { 1321 return nil, fmt.Errorf("unsupported identifier type %d", id.Type) 1322 } 1323 1324 return &corepb.Identifier{ 1325 Type: string(idType), 1326 Value: id.Value, 1327 }, nil 1328 } 1329 1330 func newIdentifierModelsFromPB(pbs []*corepb.Identifier) ([]identifierModel, error) { 1331 ids := make([]identifierModel, 0, len(pbs)) 1332 for _, pb := range pbs { 1333 id, err := newIdentifierModelFromPB(pb) 1334 if err != nil { 1335 return nil, err 1336 } 1337 ids = append(ids, id) 1338 } 1339 return ids, nil 1340 } 1341 1342 func newPBFromIdentifierModels(ids []identifierModel) (*sapb.Identifiers, error) { 1343 pbs := make([]*corepb.Identifier, 0, len(ids)) 1344 for _, id := range ids { 1345 pb, err := newPBFromIdentifierModel(id) 1346 if err != nil { 1347 return nil, err 1348 } 1349 pbs = append(pbs, pb) 1350 } 1351 return &sapb.Identifiers{Identifiers: pbs}, nil 1352 } 1353 1354 // buildIdentifierQueryConditions takes a slice of identifiers and returns a 1355 // string (conditions to use within the prepared statement) and a slice of anys 1356 // (arguments for the prepared statement), both to use within a WHERE clause for 1357 // queries against the authz2 table. 1358 // 1359 // Although this function takes user-controlled input, it does not include any 1360 // of that input directly in the returned SQL string. The resulting string 1361 // contains only column names, boolean operators, and questionmark placeholders. 1362 func buildIdentifierQueryConditions(idents identifier.ACMEIdentifiers) (string, []any) { 1363 if len(idents) == 0 { 1364 // No identifier values to check. 1365 return "FALSE", []any{} 1366 } 1367 1368 identsByType := map[identifier.IdentifierType][]string{} 1369 for _, id := range idents { 1370 identsByType[id.Type] = append(identsByType[id.Type], id.Value) 1371 } 1372 1373 var conditions []string 1374 var args []any 1375 for idType, idValues := range identsByType { 1376 conditions = append(conditions, 1377 fmt.Sprintf("identifierType = ? AND identifierValue IN (%s)", 1378 db.QuestionMarks(len(idValues)), 1379 ), 1380 ) 1381 args = append(args, identifierTypeToUint[string(idType)]) 1382 for _, idValue := range idValues { 1383 args = append(args, idValue) 1384 } 1385 } 1386 1387 return strings.Join(conditions, " OR "), args 1388 } 1389 1390 // pausedModel represents a row in the paused table. It contains the 1391 // registrationID of the paused account, the time the (account, identifier) pair 1392 // was paused, and the time the pair was unpaused. The UnpausedAt field is 1393 // nullable because the pair may not have been unpaused yet. A pair is 1394 // considered paused if there is a matching row in the paused table with a NULL 1395 // UnpausedAt time. 1396 type pausedModel struct { 1397 identifierModel 1398 RegistrationID int64 `db:"registrationID"` 1399 PausedAt time.Time `db:"pausedAt"` 1400 UnpausedAt *time.Time `db:"unpausedAt"` 1401 } 1402 1403 type overrideModel struct { 1404 LimitEnum int64 `db:"limitEnum"` 1405 BucketKey string `db:"bucketKey"` 1406 Comment string `db:"comment"` 1407 PeriodNS int64 `db:"periodNS"` 1408 Count int64 `db:"count"` 1409 Burst int64 `db:"burst"` 1410 UpdatedAt time.Time `db:"updatedAt"` 1411 Enabled bool `db:"enabled"` 1412 } 1413 1414 func overrideModelForPB(pb *sapb.RateLimitOverride, updatedAt time.Time, enabled bool) overrideModel { 1415 return overrideModel{ 1416 LimitEnum: pb.LimitEnum, 1417 BucketKey: pb.BucketKey, 1418 Comment: pb.Comment, 1419 PeriodNS: pb.Period.AsDuration().Nanoseconds(), 1420 Count: pb.Count, 1421 Burst: pb.Burst, 1422 UpdatedAt: updatedAt, 1423 Enabled: enabled, 1424 } 1425 } 1426 1427 func newPBFromOverrideModel(m *overrideModel) *sapb.RateLimitOverride { 1428 return &sapb.RateLimitOverride{ 1429 LimitEnum: m.LimitEnum, 1430 BucketKey: m.BucketKey, 1431 Comment: m.Comment, 1432 Period: durationpb.New(time.Duration(m.PeriodNS)), 1433 Count: m.Count, 1434 Burst: m.Burst, 1435 } 1436 }