Skip to content

Commit

Permalink
Allow old certs to be cross-signed (#16494)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Add changelog

Signed-off-by: Alexander Scheel <[email protected]>

* Address review feedback, fix tests

Also adds a known-answer test using LE R3 CA's SKID.

Signed-off-by: Alexander Scheel <[email protected]>

* Address review feedback regarding separators

Signed-off-by: Alexander Scheel <[email protected]>
  • Loading branch information
cipherboy authored Aug 3, 2022
1 parent b4e8151 commit 74d68e2
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 4 deletions.
7 changes: 7 additions & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
Expand Down Expand Up @@ -583,6 +584,8 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s
"signature_bits": 512,
"format": "der",
"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 {
certString := resp.Data["certificate"].(string)
Expand All @@ -602,6 +605,8 @@ func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s
}
cert := certs[0]

skid, _ := hex.DecodeString("142EB317B75856CBAE500940E61FAF9D8B14C2C6")

switch {
case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL):
return fmt.Errorf("IssuingCertificateURL:\nexpected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL)
Expand All @@ -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)
case !reflect.DeepEqual(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 {
Expand Down
23 changes: 23 additions & 0 deletions builtin/logical/pki/cert_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
Expand Down Expand Up @@ -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{
Params: &certutil.CreationParameters{
Subject: subject,
Expand All @@ -1359,6 +1381,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
BasicConstraintsValidForNonCA: data.role.BasicConstraintsValidForNonCA,
NotBeforeDuration: data.role.NotBeforeDuration,
ForceAppendCaChain: caSign != nil,
SKID: skid,
},
SigningBundle: caSign,
CSR: csr,
Expand Down
24 changes: 24 additions & 0 deletions builtin/logical/pki/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package pki

import (
"bytes"
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"fmt"
"strconv"
Expand Down Expand Up @@ -113,6 +115,7 @@ type CBGenerateIntermediate struct {
Existing bool
Name string
CommonName string
SKID string
Parent string
ImportErrorMessage string
}
Expand Down Expand Up @@ -151,13 +154,32 @@ func (c CBGenerateIntermediate) Run(t testing.TB, b *backend, s logical.Storage,
if len(c.CommonName) > 0 {
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)
if err != nil {
t.Fatalf("failed to sign CSR for issuer (%v): %v / body: %v", c.Name, err, data)
}

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
url = "intermediate/set-signed"
data = make(map[string]interface{})
Expand Down Expand Up @@ -829,6 +851,7 @@ var chainBuildingTestCases = []CBTestScenario{
Existing: true,
Name: "cross-old-new",
CommonName: "root-new",
SKID: "root-new-a",
// Which old issuer is used here doesn't matter as they have
// the same CN and key.
Parent: "root-old-a",
Expand Down Expand Up @@ -887,6 +910,7 @@ var chainBuildingTestCases = []CBTestScenario{
Existing: true,
Name: "cross-new-old",
CommonName: "root-old",
SKID: "root-old-a",
// Which new issuer is used here doesn't matter as they have
// the same CN and key.
Parent: "root-new-a",
Expand Down
24 changes: 21 additions & 3 deletions builtin/logical/pki/path_sign_issuers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import (

func pathIssuerSignIntermediate(b *backend) *framework.Path {
pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/sign-intermediate"
return pathIssuerSignIntermediateRaw(b, pattern)
return buildPathIssuerSignIntermediateRaw(b, pattern)
}

func pathSignIntermediate(b *backend) *framework.Path {
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{})
path := &framework.Path{
Pattern: pattern,
Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions changelog/16494.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secret/pki: Allow specifying SKID for cross-signed issuance from older Vault versions.
```
12 changes: 11 additions & 1 deletion sdk/helper/certutil/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) {
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) {
var publicKeyBytes []byte
switch pub := pub.(type) {
Expand Down Expand Up @@ -1066,7 +1076,7 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun
return nil, err
}

subjKeyID, err := getSubjectKeyID(data.CSR.PublicKey)
subjKeyID, err := getSubjectKeyIDFromBundle(data)
if err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions sdk/helper/certutil/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,9 @@ type CreationParameters struct {

// The duration the certificate will use NotBefore
NotBeforeDuration time.Duration

// The explicit SKID to use; especially useful for cross-signing.
SKID []byte
}

type CreationBundle struct {
Expand Down
10 changes: 10 additions & 0 deletions website/content/api-docs/secret/pki.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,16 @@ when signing an externally-owned intermediate.
`signature_bits` value; only RSA issuers will change signature types
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

```json
Expand Down

0 comments on commit 74d68e2

Please sign in to comment.