package pki import ( "context" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/pem" "errors" "fmt" "math/big" "strconv" "strings" "time" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/logical" ) const ( crlNumberParam = "crl_number" deltaCrlBaseNumberParam = "delta_crl_base_number" nextUpdateParam = "next_update" crlsParam = "crls" formatParam = "format" ) var ( akOid = asn1.ObjectIdentifier{2, 5, 29, 35} crlNumOid = asn1.ObjectIdentifier{2, 5, 29, 20} deltaCrlOid = asn1.ObjectIdentifier{2, 5, 29, 27} ) func pathResignCrls(b *backend) *framework.Path { return &framework.Path{ Pattern: "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/resign-crls", Fields: map[string]*framework.FieldSchema{ issuerRefParam: { Type: framework.TypeString, Description: `Reference to a existing issuer; either "default" for the configured default issuer, an identifier or the name assigned to the issuer.`, Default: defaultRef, }, crlNumberParam: { Type: framework.TypeInt, Description: `The sequence number to be written within the CRL Number extension.`, }, deltaCrlBaseNumberParam: { Type: framework.TypeInt, Description: `Using a zero or greater value specifies the base CRL revision number to encode within a Delta CRL indicator extension, otherwise the extension will not be added.`, Default: -1, }, nextUpdateParam: { Type: framework.TypeString, Description: `The amount of time the generated CRL should be valid; defaults to 72 hours.`, Default: defaultCrlConfig.Expiry, }, crlsParam: { Type: framework.TypeStringSlice, Description: `A list of PEM encoded CRLs to combine, originally signed by the requested issuer.`, }, formatParam: { Type: framework.TypeString, Description: `The format of the combined CRL, can be "pem" or "der". If "der", the value will be base64 encoded. Defaults to "pem".`, Default: "pem", }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.UpdateOperation: &framework.PathOperation{ Callback: b.pathUpdateResignCrlsHandler, }, }, HelpSynopsis: `Combine and sign with the provided issuer different CRLs`, HelpDescription: `Provide two or more PEM encoded CRLs signed by the issuer, normally from separate Vault clusters to be combined and signed.`, } } func pathSignRevocationList(b *backend) *framework.Path { return &framework.Path{ Pattern: "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/sign-revocation-list", Fields: map[string]*framework.FieldSchema{ issuerRefParam: { Type: framework.TypeString, Description: `Reference to a existing issuer; either "default" for the configured default issuer, an identifier or the name assigned to the issuer.`, Default: defaultRef, }, crlNumberParam: { Type: framework.TypeInt, Description: `The sequence number to be written within the CRL Number extension.`, }, deltaCrlBaseNumberParam: { Type: framework.TypeInt, Description: `Using a zero or greater value specifies the base CRL revision number to encode within a Delta CRL indicator extension, otherwise the extension will not be added.`, Default: -1, }, nextUpdateParam: { Type: framework.TypeString, Description: `The amount of time the generated CRL should be valid; defaults to 72 hours.`, Default: defaultCrlConfig.Expiry, }, formatParam: { Type: framework.TypeString, Description: `The format of the combined CRL, can be "pem" or "der". If "der", the value will be base64 encoded. Defaults to "pem".`, Default: "pem", }, "revoked_certs": { Type: framework.TypeSlice, Description: `A list of maps containing the keys serial_number (string), revocation_time (string), and extensions (map with keys id (string), critical (bool), value (string))`, }, "extensions": { Type: framework.TypeSlice, Description: `A list of maps containing extensions with keys id (string), critical (bool), value (string)`, }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.UpdateOperation: &framework.PathOperation{ Callback: b.pathUpdateSignRevocationListHandler, }, }, HelpSynopsis: `Generate and sign a CRL based on the provided parameters.`, HelpDescription: `Given a list of revoked certificates and other parameters, return a signed CRL based on the parameter values.`, } } func (b *backend) pathUpdateResignCrlsHandler(ctx context.Context, request *logical.Request, data *framework.FieldData) (*logical.Response, error) { if b.useLegacyBundleCaStorage() { return logical.ErrorResponse("This API cannot be used until the migration has completed"), nil } issuerRef := getIssuerRef(data) crlNumber := data.Get(crlNumberParam).(int) deltaCrlBaseNumber := data.Get(deltaCrlBaseNumberParam).(int) nextUpdateStr := data.Get(nextUpdateParam).(string) rawCrls := data.Get(crlsParam).([]string) format, err := parseCrlFormat(data.Get(formatParam).(string)) if err != nil { return logical.ErrorResponse(err.Error()), nil } nextUpdateOffset, err := parseutil.ParseDurationSecond(nextUpdateStr) if err != nil { return logical.ErrorResponse("invalid value for %s: %v", nextUpdateParam, err), nil } if nextUpdateOffset <= 0 { return logical.ErrorResponse("%s parameter must be greater than 0", nextUpdateParam), nil } if crlNumber < 0 { return logical.ErrorResponse("%s parameter must be 0 or greater", crlNumberParam), nil } if deltaCrlBaseNumber < -1 { return logical.ErrorResponse("%s parameter must be -1 or greater", deltaCrlBaseNumberParam), nil } if issuerRef == "" { return logical.ErrorResponse("%s parameter cannot be blank", issuerRefParam), nil } providedCrls, err := decodePemCrls(rawCrls) if err != nil { return logical.ErrorResponse(err.Error()), nil } sc := b.makeStorageContext(ctx, request.Storage) caBundle, err := getCaBundle(sc, issuerRef) if err != nil { return logical.ErrorResponse(err.Error()), nil } if err := verifyCrlsAreFromIssuersKey(caBundle.Certificate, providedCrls); err != nil { return logical.ErrorResponse(err.Error()), nil } revokedCerts, warnings, err := getAllRevokedCertsFromPem(providedCrls) if err != nil { return logical.ErrorResponse(err.Error()), nil } now := time.Now() template := &x509.RevocationList{ SignatureAlgorithm: caBundle.RevocationSigAlg, RevokedCertificates: revokedCerts, Number: big.NewInt(int64(crlNumber)), ThisUpdate: now, NextUpdate: now.Add(nextUpdateOffset), } if deltaCrlBaseNumber > -1 { ext, err := certutil.CreateDeltaCRLIndicatorExt(int64(deltaCrlBaseNumber)) if err != nil { return nil, fmt.Errorf("could not create crl delta indicator extension: %v", err) } template.ExtraExtensions = []pkix.Extension{ext} } crlBytes, err := x509.CreateRevocationList(rand.Reader, template, caBundle.Certificate, caBundle.PrivateKey) if err != nil { return nil, fmt.Errorf("error creating new CRL: %w", err) } body := encodeResponse(crlBytes, format == "der") return &logical.Response{ Warnings: warnings, Data: map[string]interface{}{ "crl": body, }, }, nil } func (b *backend) pathUpdateSignRevocationListHandler(ctx context.Context, request *logical.Request, data *framework.FieldData) (*logical.Response, error) { if b.useLegacyBundleCaStorage() { return logical.ErrorResponse("This API cannot be used until the migration has completed"), nil } issuerRef := getIssuerRef(data) crlNumber := data.Get(crlNumberParam).(int) deltaCrlBaseNumber := data.Get(deltaCrlBaseNumberParam).(int) nextUpdateStr := data.Get(nextUpdateParam).(string) nextUpdateOffset, err := parseutil.ParseDurationSecond(nextUpdateStr) if err != nil { return logical.ErrorResponse("invalid value for %s: %v", nextUpdateParam, err), nil } if nextUpdateOffset <= 0 { return logical.ErrorResponse("%s parameter must be greater than 0", nextUpdateParam), nil } if crlNumber < 0 { return logical.ErrorResponse("%s parameter must be 0 or greater", crlNumberParam), nil } if deltaCrlBaseNumber < -1 { return logical.ErrorResponse("%s parameter must be -1 or greater", deltaCrlBaseNumberParam), nil } if issuerRef == "" { return logical.ErrorResponse("%s parameter cannot be blank", issuerRefParam), nil } format, err := parseCrlFormat(data.Get(formatParam).(string)) if err != nil { return logical.ErrorResponse(err.Error()), nil } revokedCerts, err := parseRevokedCertsParam(data.Get("revoked_certs").([]interface{})) if err != nil { return logical.ErrorResponse(err.Error()), nil } crlExtensions, err := parseExtensionsParam(data.Get("extensions").([]interface{})) if err != nil { return logical.ErrorResponse(err.Error()), nil } sc := b.makeStorageContext(ctx, request.Storage) caBundle, err := getCaBundle(sc, issuerRef) if err != nil { return logical.ErrorResponse(err.Error()), nil } if deltaCrlBaseNumber > -1 { ext, err := certutil.CreateDeltaCRLIndicatorExt(int64(deltaCrlBaseNumber)) if err != nil { return nil, fmt.Errorf("could not create crl delta indicator extension: %v", err) } crlExtensions = append(crlExtensions, ext) } now := time.Now() template := &x509.RevocationList{ SignatureAlgorithm: caBundle.RevocationSigAlg, RevokedCertificates: revokedCerts, Number: big.NewInt(int64(crlNumber)), ThisUpdate: now, NextUpdate: now.Add(nextUpdateOffset), ExtraExtensions: crlExtensions, } crlBytes, err := x509.CreateRevocationList(rand.Reader, template, caBundle.Certificate, caBundle.PrivateKey) if err != nil { return nil, fmt.Errorf("error creating new CRL: %w", err) } body := encodeResponse(crlBytes, format == "der") return &logical.Response{ Data: map[string]interface{}{ "crl": body, }, }, nil } func parseRevokedCertsParam(revokedCerts []interface{}) ([]pkix.RevokedCertificate, error) { var parsedCerts []pkix.RevokedCertificate seenSerials := make(map[*big.Int]int) for i, entry := range revokedCerts { if revokedCert, ok := entry.(map[string]interface{}); ok { serialNum, err := parseSerialNum(revokedCert) if err != nil { return nil, fmt.Errorf("failed parsing serial_number from entry %d: %w", i, err) } if origEntry, exists := seenSerials[serialNum]; exists { serialNumStr := revokedCert["serial_number"] return nil, fmt.Errorf("duplicate serial number: %s, original entry %d and %d", serialNumStr, origEntry, i) } seenSerials[serialNum] = i revocationTime, err := parseRevocationTime(revokedCert) if err != nil { return nil, fmt.Errorf("failed parsing revocation_time from entry %d: %w", i, err) } extensions, err := parseCertExtensions(revokedCert) if err != nil { return nil, fmt.Errorf("failed parsing extensions from entry %d: %w", i, err) } parsedCerts = append(parsedCerts, pkix.RevokedCertificate{ SerialNumber: serialNum, RevocationTime: revocationTime, Extensions: extensions, }) } } return parsedCerts, nil } func parseCertExtensions(cert map[string]interface{}) ([]pkix.Extension, error) { extRaw, exists := cert["extensions"] if !exists || extRaw == nil || extRaw == "" { // We don't require extensions to be populated return []pkix.Extension{}, nil } extListRaw, ok := extRaw.([]interface{}) if !ok { return nil, errors.New("'extensions' field did not contain a slice") } return parseExtensionsParam(extListRaw) } func parseExtensionsParam(extRawList []interface{}) ([]pkix.Extension, error) { var extensions []pkix.Extension seenOid := make(map[string]struct{}) for i, entryRaw := range extRawList { entry, ok := entryRaw.(map[string]interface{}) if !ok { return nil, fmt.Errorf("extension entry %d not a map", i) } extension, err := parseExtension(entry) if err != nil { return nil, fmt.Errorf("failed parsing extension entry %d: %w", i, err) } parsedIdStr := extension.Id.String() if _, exists := seenOid[parsedIdStr]; exists { return nil, fmt.Errorf("duplicate extension id: %s", parsedIdStr) } seenOid[parsedIdStr] = struct{}{} extensions = append(extensions, extension) } return extensions, nil } func parseExtension(entry map[string]interface{}) (pkix.Extension, error) { asnObjectId, err := parseExtAsn1ObjectId(entry) if err != nil { return pkix.Extension{}, err } if asnObjectId.Equal(akOid) { return pkix.Extension{}, fmt.Errorf("authority key object identifier (%s) is reserved", akOid.String()) } if asnObjectId.Equal(crlNumOid) { return pkix.Extension{}, fmt.Errorf("crl number object identifier (%s) is reserved", crlNumOid.String()) } if asnObjectId.Equal(deltaCrlOid) { return pkix.Extension{}, fmt.Errorf("delta crl object identifier (%s) is reserved", deltaCrlOid.String()) } critical, err := parseExtCritical(entry) if err != nil { return pkix.Extension{}, err } extVal, err := parseExtValue(entry) if err != nil { return pkix.Extension{}, err } return pkix.Extension{ Id: asnObjectId, Critical: critical, Value: extVal, }, nil } func parseExtValue(entry map[string]interface{}) ([]byte, error) { valRaw, exists := entry["value"] if !exists { return nil, errors.New("missing 'value' field") } valStr, err := parseutil.ParseString(valRaw) if err != nil { return nil, fmt.Errorf("'value' field value was not a string: %w", err) } if len(valStr) == 0 { return []byte{}, nil } decodeString, err := base64.StdEncoding.DecodeString(valStr) if err != nil { return nil, fmt.Errorf("failed base64 decoding 'value' field: %w", err) } return decodeString, nil } func parseExtCritical(entry map[string]interface{}) (bool, error) { critRaw, exists := entry["critical"] if !exists || critRaw == nil || critRaw == "" { // Optional field, so just return as if they provided the value false. return false, nil } myBool, err := parseutil.ParseBool(critRaw) if err != nil { return false, fmt.Errorf("critical field value failed to be parsed: %w", err) } return myBool, nil } func parseExtAsn1ObjectId(entry map[string]interface{}) (asn1.ObjectIdentifier, error) { idRaw, idExists := entry["id"] if !idExists { return asn1.ObjectIdentifier{}, errors.New("missing id field") } oidStr, err := parseutil.ParseString(idRaw) if err != nil { return nil, fmt.Errorf("'id' field value was not a string: %w", err) } if len(oidStr) == 0 { return asn1.ObjectIdentifier{}, errors.New("zero length object identifier") } // Parse out dot notation oidParts := strings.Split(oidStr, ".") oid := make(asn1.ObjectIdentifier, len(oidParts), len(oidParts)) for i := range oidParts { oidIntVal, err := strconv.Atoi(oidParts[i]) if err != nil { return nil, fmt.Errorf("failed parsing asn1 index element %d value %s: %w", i, oidParts[i], err) } oid[i] = oidIntVal } return oid, nil } func parseRevocationTime(cert map[string]interface{}) (time.Time, error) { var revTime time.Time revTimeRaw, exists := cert["revocation_time"] if !exists { return revTime, errors.New("missing 'revocation_time' field") } revTime, err := parseutil.ParseAbsoluteTime(revTimeRaw) if err != nil { return revTime, fmt.Errorf("failed parsing time %v: %w", revTimeRaw, err) } return revTime, nil } func parseSerialNum(cert map[string]interface{}) (*big.Int, error) { serialNumRaw, serialExists := cert["serial_number"] if !serialExists { return nil, errors.New("missing 'serial_number' field") } serialNumStr, err := parseutil.ParseString(serialNumRaw) if err != nil { return nil, fmt.Errorf("'serial_number' field value was not a string: %w", err) } // Clean up any provided serials to decoder for _, separator := range []string{":", ".", "-", " "} { serialNumStr = strings.ReplaceAll(serialNumStr, separator, "") } // Prefer hex.DecodeString over certutil.ParseHexFormatted as we don't need a separator serialBytes, err := hex.DecodeString(serialNumStr) if err != nil { return nil, fmt.Errorf("'serial_number' failed converting to bytes: %w", err) } bigIntSerial := big.Int{} bigIntSerial.SetBytes(serialBytes) return &bigIntSerial, nil } func parseCrlFormat(requestedValue string) (string, error) { format := strings.ToLower(requestedValue) switch format { case "pem", "der": return format, nil default: return "", fmt.Errorf("unknown format value of %s", requestedValue) } } func verifyCrlsAreFromIssuersKey(caCert *x509.Certificate, crls []*x509.RevocationList) error { for i, crl := range crls { // At this point we assume if the issuer's key signed the CRL that is a good enough check // to validate that we owned/generated the provided CRL. if err := crl.CheckSignatureFrom(caCert); err != nil { return fmt.Errorf("CRL index: %d was not signed by requested issuer", i) } } return nil } func encodeResponse(crlBytes []byte, derFormatRequested bool) string { if derFormatRequested { return base64.StdEncoding.EncodeToString(crlBytes) } block := pem.Block{ Type: "X509 CRL", Bytes: crlBytes, } return string(pem.EncodeToMemory(&block)) } func getAllRevokedCertsFromPem(crls []*x509.RevocationList) ([]pkix.RevokedCertificate, []string, error) { uniqueCert := map[string]pkix.RevokedCertificate{} var warnings []string for _, crl := range crls { for _, curCert := range crl.RevokedCertificates { serial := serialFromBigInt(curCert.SerialNumber) // Get rid of any extensions the existing certificate might have had. curCert.Extensions = []pkix.Extension{} existingCert, exists := uniqueCert[serial] if !exists { // First time we see the revoked cert uniqueCert[serial] = curCert continue } if existingCert.RevocationTime.Equal(curCert.RevocationTime) { // Same revocation times, just skip it continue } warn := fmt.Sprintf("Duplicate serial %s with different revocation "+ "times detected, using oldest revocation time", serial) warnings = append(warnings, warn) if existingCert.RevocationTime.After(curCert.RevocationTime) { uniqueCert[serial] = curCert } } } var revokedCerts []pkix.RevokedCertificate for _, cert := range uniqueCert { revokedCerts = append(revokedCerts, cert) } return revokedCerts, warnings, nil } func getCaBundle(sc *storageContext, issuerRef string) (*certutil.CAInfoBundle, error) { issuerId, err := sc.resolveIssuerReference(issuerRef) if err != nil { return nil, fmt.Errorf("failed to resolve issuer %s: %w", issuerRefParam, err) } return sc.fetchCAInfoByIssuerId(issuerId, CRLSigningUsage) } func decodePemCrls(rawCrls []string) ([]*x509.RevocationList, error) { var crls []*x509.RevocationList for i, rawCrl := range rawCrls { crl, err := decodePemCrl(rawCrl) if err != nil { return nil, fmt.Errorf("failed decoding crl %d: %w", i, err) } crls = append(crls, crl) } return crls, nil } func decodePemCrl(crl string) (*x509.RevocationList, error) { block, rest := pem.Decode([]byte(crl)) if len(rest) != 0 { return nil, errors.New("invalid crl; should be one PEM block only") } return x509.ParseRevocationList(block.Bytes) }