Allow old certs to be cross-signed (#16494)
* Allow old certs to be cross-signed In Vault 1.11, we introduced cross-signing support, but the earlier SKID field change in Vault 1.10 causes problems: notably, certs created on older versions of Vault (<=1.9) or outside of Vault (with a different SKID method) cannot be cross-signed and validated in OpenSSL. In particular, OpenSSL appears to be unique in requiring a SKID/AKID match for chain building. If AKID and SKID are present on an otherwise valid client/parent cert pair and the values are different, OpenSSL will not build a valid path over those two, whereas most other chain validation implementations will. Regardless, to have proper cross-signing support, we really aught to support copying an SKID. This adds such support to the sign-intermediate endpoint. Support for the /issue endpoint is not added, as cross-signing leaf certs isn't generally useful and can accept random SKIDs. Resolves: #16461 Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Address review feedback, fix tests Also adds a known-answer test using LE R3 CA's SKID. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Address review feedback regarding separators Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
parent
dc0b4315f3
commit
cf7105929f
|
@ -12,6 +12,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -583,6 +584,8 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s
|
||||||
"signature_bits": 512,
|
"signature_bits": 512,
|
||||||
"format": "der",
|
"format": "der",
|
||||||
"not_before_duration": "2h",
|
"not_before_duration": "2h",
|
||||||
|
// Let's Encrypt -- R3 SKID
|
||||||
|
"skid": "14:2E:B3:17:B7:58:56:CB:AE:50:09:40:E6:1F:AF:9D:8B:14:C2:C6",
|
||||||
},
|
},
|
||||||
Check: func(resp *logical.Response) error {
|
Check: func(resp *logical.Response) error {
|
||||||
certString := resp.Data["certificate"].(string)
|
certString := resp.Data["certificate"].(string)
|
||||||
|
@ -602,6 +605,8 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s
|
||||||
}
|
}
|
||||||
cert := certs[0]
|
cert := certs[0]
|
||||||
|
|
||||||
|
skid, _ := hex.DecodeString("142EB317B75856CBAE500940E61FAF9D8B14C2C6")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL):
|
case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL):
|
||||||
return fmt.Errorf("IssuingCertificateURL:\nexpected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL)
|
return fmt.Errorf("IssuingCertificateURL:\nexpected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL)
|
||||||
|
@ -613,6 +618,8 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s
|
||||||
return fmt.Errorf("DNSNames\nexpected\n%#v\ngot\n%#v\n", []string{"intermediate.cert.com"}, cert.DNSNames)
|
return fmt.Errorf("DNSNames\nexpected\n%#v\ngot\n%#v\n", []string{"intermediate.cert.com"}, cert.DNSNames)
|
||||||
case !reflect.DeepEqual(x509.SHA512WithRSA, cert.SignatureAlgorithm):
|
case !reflect.DeepEqual(x509.SHA512WithRSA, cert.SignatureAlgorithm):
|
||||||
return fmt.Errorf("Signature Algorithm:\nexpected\n%#v\ngot\n%#v\n", x509.SHA512WithRSA, cert.SignatureAlgorithm)
|
return fmt.Errorf("Signature Algorithm:\nexpected\n%#v\ngot\n%#v\n", x509.SHA512WithRSA, cert.SignatureAlgorithm)
|
||||||
|
case !reflect.DeepEqual(skid, cert.SubjectKeyId):
|
||||||
|
return fmt.Errorf("SKID:\nexpected\n%#v\ngot\n%#v\n", skid, cert.SubjectKeyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 {
|
if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -1340,6 +1341,27 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse SKID from the request for cross-signing.
|
||||||
|
var skid []byte
|
||||||
|
{
|
||||||
|
if rawSKIDValue, ok := data.apiData.GetOk("skid"); ok {
|
||||||
|
// Handle removing common separators to make copy/paste from tool
|
||||||
|
// output easier. Chromium uses space, OpenSSL uses colons, and at
|
||||||
|
// one point, Vault had preferred dash as a separator for hex
|
||||||
|
// strings.
|
||||||
|
var err error
|
||||||
|
skidValue := rawSKIDValue.(string)
|
||||||
|
for _, separator := range []string{":", "-", " "} {
|
||||||
|
skidValue = strings.ReplaceAll(skidValue, separator, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
skid, err = hex.DecodeString(skidValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errutil.UserError{Err: fmt.Sprintf("cannot parse requested SKID value as hex: %v", err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
creation := &certutil.CreationBundle{
|
creation := &certutil.CreationBundle{
|
||||||
Params: &certutil.CreationParameters{
|
Params: &certutil.CreationParameters{
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
|
@ -1359,6 +1381,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
|
||||||
BasicConstraintsValidForNonCA: data.role.BasicConstraintsValidForNonCA,
|
BasicConstraintsValidForNonCA: data.role.BasicConstraintsValidForNonCA,
|
||||||
NotBeforeDuration: data.role.NotBeforeDuration,
|
NotBeforeDuration: data.role.NotBeforeDuration,
|
||||||
ForceAppendCaChain: caSign != nil,
|
ForceAppendCaChain: caSign != nil,
|
||||||
|
SKID: skid,
|
||||||
},
|
},
|
||||||
SigningBundle: caSign,
|
SigningBundle: caSign,
|
||||||
CSR: csr,
|
CSR: csr,
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package pki
|
package pki
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -113,6 +115,7 @@ type CBGenerateIntermediate struct {
|
||||||
Existing bool
|
Existing bool
|
||||||
Name string
|
Name string
|
||||||
CommonName string
|
CommonName string
|
||||||
|
SKID string
|
||||||
Parent string
|
Parent string
|
||||||
ImportErrorMessage string
|
ImportErrorMessage string
|
||||||
}
|
}
|
||||||
|
@ -151,6 +154,14 @@ func (c CBGenerateIntermediate) Run(t testing.TB, b *backend, s logical.Storage,
|
||||||
if len(c.CommonName) > 0 {
|
if len(c.CommonName) > 0 {
|
||||||
data["common_name"] = c.CommonName
|
data["common_name"] = c.CommonName
|
||||||
}
|
}
|
||||||
|
if len(c.SKID) > 0 {
|
||||||
|
// Copy the SKID from an existing, already-issued cert.
|
||||||
|
otherPEM := knownCerts[c.SKID]
|
||||||
|
otherCert := ToCertificate(t, otherPEM)
|
||||||
|
|
||||||
|
data["skid"] = hex.EncodeToString(otherCert.SubjectKeyId)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err = CBWrite(b, s, url, data)
|
resp, err = CBWrite(b, s, url, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to sign CSR for issuer (%v): %v / body: %v", c.Name, err, data)
|
t.Fatalf("failed to sign CSR for issuer (%v): %v / body: %v", c.Name, err, data)
|
||||||
|
@ -158,6 +169,17 @@ func (c CBGenerateIntermediate) Run(t testing.TB, b *backend, s logical.Storage,
|
||||||
|
|
||||||
knownCerts[c.Name] = strings.TrimSpace(resp.Data["certificate"].(string))
|
knownCerts[c.Name] = strings.TrimSpace(resp.Data["certificate"].(string))
|
||||||
|
|
||||||
|
// Verify SKID if one was requested.
|
||||||
|
if len(c.SKID) > 0 {
|
||||||
|
otherPEM := knownCerts[c.SKID]
|
||||||
|
otherCert := ToCertificate(t, otherPEM)
|
||||||
|
ourCert := ToCertificate(t, knownCerts[c.Name])
|
||||||
|
|
||||||
|
if !bytes.Equal(otherCert.SubjectKeyId, ourCert.SubjectKeyId) {
|
||||||
|
t.Fatalf("Expected two certs to have equal SKIDs but differed: them: %v vs us: %v", otherCert.SubjectKeyId, ourCert.SubjectKeyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set the signed intermediate
|
// Set the signed intermediate
|
||||||
url = "intermediate/set-signed"
|
url = "intermediate/set-signed"
|
||||||
data = make(map[string]interface{})
|
data = make(map[string]interface{})
|
||||||
|
@ -829,6 +851,7 @@ var chainBuildingTestCases = []CBTestScenario{
|
||||||
Existing: true,
|
Existing: true,
|
||||||
Name: "cross-old-new",
|
Name: "cross-old-new",
|
||||||
CommonName: "root-new",
|
CommonName: "root-new",
|
||||||
|
SKID: "root-new-a",
|
||||||
// Which old issuer is used here doesn't matter as they have
|
// Which old issuer is used here doesn't matter as they have
|
||||||
// the same CN and key.
|
// the same CN and key.
|
||||||
Parent: "root-old-a",
|
Parent: "root-old-a",
|
||||||
|
@ -887,6 +910,7 @@ var chainBuildingTestCases = []CBTestScenario{
|
||||||
Existing: true,
|
Existing: true,
|
||||||
Name: "cross-new-old",
|
Name: "cross-new-old",
|
||||||
CommonName: "root-old",
|
CommonName: "root-old",
|
||||||
|
SKID: "root-old-a",
|
||||||
// Which new issuer is used here doesn't matter as they have
|
// Which new issuer is used here doesn't matter as they have
|
||||||
// the same CN and key.
|
// the same CN and key.
|
||||||
Parent: "root-new-a",
|
Parent: "root-new-a",
|
||||||
|
|
|
@ -7,15 +7,15 @@ import (
|
||||||
|
|
||||||
func pathIssuerSignIntermediate(b *backend) *framework.Path {
|
func pathIssuerSignIntermediate(b *backend) *framework.Path {
|
||||||
pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/sign-intermediate"
|
pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/sign-intermediate"
|
||||||
return pathIssuerSignIntermediateRaw(b, pattern)
|
return buildPathIssuerSignIntermediateRaw(b, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathSignIntermediate(b *backend) *framework.Path {
|
func pathSignIntermediate(b *backend) *framework.Path {
|
||||||
pattern := "root/sign-intermediate"
|
pattern := "root/sign-intermediate"
|
||||||
return pathIssuerSignIntermediateRaw(b, pattern)
|
return buildPathIssuerSignIntermediateRaw(b, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathIssuerSignIntermediateRaw(b *backend, pattern string) *framework.Path {
|
func buildPathIssuerSignIntermediateRaw(b *backend, pattern string) *framework.Path {
|
||||||
fields := addIssuerRefField(map[string]*framework.FieldSchema{})
|
fields := addIssuerRefField(map[string]*framework.FieldSchema{})
|
||||||
path := &framework.Path{
|
path := &framework.Path{
|
||||||
Pattern: pattern,
|
Pattern: pattern,
|
||||||
|
@ -67,6 +67,24 @@ SHA-2-512. Defaults to 0 to automatically detect based on key length
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields["skid"] = &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Default: "",
|
||||||
|
Description: `Value for the Subject Key Identifier field
|
||||||
|
(RFC 5280 Section 4.2.1.2). This value should ONLY be used when
|
||||||
|
cross-signing to mimic the existing certificate's SKID value; this
|
||||||
|
is necessary to allow certain TLS implementations (such as OpenSSL)
|
||||||
|
which use SKID/AKID matches in chain building to restrict possible
|
||||||
|
valid chains.
|
||||||
|
|
||||||
|
Specified as a string in hex format. Default is empty, allowing
|
||||||
|
Vault to automatically calculate the SKID according to method one
|
||||||
|
in the above RFC section.`,
|
||||||
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
secret/pki: Allow specifying SKID for cross-signed issuance from older Vault versions.
|
||||||
|
```
|
|
@ -87,6 +87,16 @@ func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) {
|
||||||
return getSubjectKeyID(privateKey.Public())
|
return getSubjectKeyID(privateKey.Public())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the explicit SKID when used for cross-signing, else computes a new
|
||||||
|
// SKID from the key itself.
|
||||||
|
func getSubjectKeyIDFromBundle(data *CreationBundle) ([]byte, error) {
|
||||||
|
if len(data.Params.SKID) > 0 {
|
||||||
|
return data.Params.SKID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSubjectKeyID(data.CSR.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
func getSubjectKeyID(pub interface{}) ([]byte, error) {
|
func getSubjectKeyID(pub interface{}) ([]byte, error) {
|
||||||
var publicKeyBytes []byte
|
var publicKeyBytes []byte
|
||||||
switch pub := pub.(type) {
|
switch pub := pub.(type) {
|
||||||
|
@ -1066,7 +1076,7 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
subjKeyID, err := getSubjectKeyID(data.CSR.PublicKey)
|
subjKeyID, err := getSubjectKeyIDFromBundle(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -796,6 +796,9 @@ type CreationParameters struct {
|
||||||
|
|
||||||
// The duration the certificate will use NotBefore
|
// The duration the certificate will use NotBefore
|
||||||
NotBeforeDuration time.Duration
|
NotBeforeDuration time.Duration
|
||||||
|
|
||||||
|
// The explicit SKID to use; especially useful for cross-signing.
|
||||||
|
SKID []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreationBundle struct {
|
type CreationBundle struct {
|
||||||
|
|
|
@ -571,6 +571,16 @@ when signing an externally-owned intermediate.
|
||||||
`signature_bits` value; only RSA issuers will change signature types
|
`signature_bits` value; only RSA issuers will change signature types
|
||||||
based on this parameter.
|
based on this parameter.
|
||||||
|
|
||||||
|
- `skid` `(string: "")` - Value for the Subject Key Identifier field
|
||||||
|
(RFC 5280 Section 4.2.1.2). Specified as a string in hex format. Default
|
||||||
|
is empty, allowing Vault to automatically calculate the SKID according
|
||||||
|
to method one in the above RFC section.
|
||||||
|
|
||||||
|
~> **Note**: This value should ONLY be used when cross-signing to mimic
|
||||||
|
the existing certificate's SKID value; this is necessary to allow
|
||||||
|
certain TLS implementations (such as OpenSSL) which use SKID/AKID
|
||||||
|
matches in chain building to restrict possible valid chains.
|
||||||
|
|
||||||
#### Sample Payload
|
#### Sample Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
Loading…
Reference in New Issue