Apply URL encoding/unencoding to OCSP Get requests (#18938)

* Apply URL encoding/unencoding to OCSP Get requests

 - Missed this during development and sadly the unit tests were written
   at a level that did not expose this issue originally, there are
   certain combinations of issuer cert + serial that lead to base64
   data containing a '/' which will lead to the OCSP handler not getting
   the full parameter.
 - Do as the spec says, this should be treated as url-encoded data.

* Add cl

* Add higher level PKI OCSP GET/POST tests

* Rename PKI ocsp files to path_ocsp to follow naming conventions

* make fmt
This commit is contained in:
Steven Clark 2023-02-01 11:03:43 -05:00 committed by GitHub
parent 6a8716ac18
commit baf66ff56e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 2 deletions

View File

@ -13,6 +13,7 @@ import (
"io" "io"
"math/big" "math/big"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
@ -248,7 +249,12 @@ func fetchDerEncodedRequest(request *logical.Request, data *framework.FieldData)
return nil, errors.New("request is too large") return nil, errors.New("request is too large")
} }
return base64.StdEncoding.DecodeString(base64Req) unescapedBase64, err := url.QueryUnescape(base64Req)
if err != nil {
return nil, fmt.Errorf("failed to unescape base64 string: %w", err)
}
return base64.StdEncoding.DecodeString(unescapedBase64)
case logical.UpdateOperation: case logical.UpdateOperation:
// POST bodies should contain the binary form of the DER request. // POST bodies should contain the binary form of the DER request.
// NOTE: Writing an empty update request to Vault causes a nil request.HTTPRequest, and that object // NOTE: Writing an empty update request to Vault causes a nil request.HTTPRequest, and that object

View File

@ -9,11 +9,15 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
@ -362,6 +366,102 @@ func TestOcsp_MultipleMatchingIssuersOneWithoutSigningUsage(t *testing.T) {
requireOcspResponseSignedBy(t, ocspResp, rotatedCert) requireOcspResponseSignedBy(t, ocspResp, rotatedCert)
} }
// Make sure OCSP GET/POST requests work through the entire stack, and not just
// through the quicker backend layer the other tests are doing.
func TestOcsp_HigherLevel(t *testing.T) {
t.Parallel()
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
mountPKIEndpoint(t, client, "pki")
resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"key_type": "ec",
"common_name": "root-ca.com",
"ttl": "600h",
})
require.NoError(t, err, "error generating root ca: %v", err)
require.NotNil(t, resp, "expected ca info from root")
issuerCert := parseCert(t, resp.Data["certificate"].(string))
resp, err = client.Logical().Write("pki/roles/example", map[string]interface{}{
"allowed_domains": "example.com",
"allow_subdomains": "true",
"no_store": "false", // make sure we store this cert
"max_ttl": "1h",
"key_type": "ec",
})
require.NoError(t, err, "error setting up pki role: %v", err)
resp, err = client.Logical().Write("pki/issue/example", map[string]interface{}{
"common_name": "test.example.com",
"ttl": "15m",
})
require.NoError(t, err, "error issuing certificate: %v", err)
require.NotNil(t, resp, "got nil response from issuing request")
certToRevoke := parseCert(t, resp.Data["certificate"].(string))
serialNum := resp.Data["serial_number"].(string)
// Revoke the certificate
resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{
"serial_number": serialNum,
})
require.NoError(t, err, "error revoking certificate: %v", err)
require.NotNil(t, resp, "got nil response from revoke")
// Make sure that OCSP handler responds properly
ocspReq := generateRequest(t, crypto.SHA256, certToRevoke, issuerCert)
ocspPostReq := client.NewRequest(http.MethodPost, "/v1/pki/ocsp")
ocspPostReq.Headers.Set("Content-Type", "application/ocsp-request")
ocspPostReq.BodyBytes = ocspReq
rawResp, err := client.RawRequest(ocspPostReq)
require.NoError(t, err, "failed sending ocsp post request")
require.Equal(t, 200, rawResp.StatusCode)
require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type"))
bodyReader := rawResp.Body
respDer, err := io.ReadAll(bodyReader)
bodyReader.Close()
require.NoError(t, err, "failed reading response body")
ocspResp, err := ocsp.ParseResponse(respDer, issuerCert)
require.NoError(t, err, "parsing ocsp get response")
require.Equal(t, ocsp.Revoked, ocspResp.Status)
require.Equal(t, issuerCert, ocspResp.Certificate)
require.Equal(t, certToRevoke.SerialNumber, ocspResp.SerialNumber)
// Test OCSP Get request for ocsp
urlEncoded := url.QueryEscape(base64.StdEncoding.EncodeToString(ocspReq))
ocspGetReq := client.NewRequest(http.MethodGet, "/v1/pki/ocsp/"+urlEncoded)
ocspGetReq.Headers.Set("Content-Type", "application/ocsp-request")
rawResp, err = client.RawRequest(ocspGetReq)
require.NoError(t, err, "failed sending ocsp get request")
require.Equal(t, 200, rawResp.StatusCode)
require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type"))
bodyReader = rawResp.Body
respDer, err = io.ReadAll(bodyReader)
bodyReader.Close()
require.NoError(t, err, "failed reading response body")
ocspResp, err = ocsp.ParseResponse(respDer, issuerCert)
require.NoError(t, err, "parsing ocsp get response")
require.Equal(t, ocsp.Revoked, ocspResp.Status)
require.Equal(t, issuerCert, ocspResp.Certificate)
require.Equal(t, certToRevoke.SerialNumber, ocspResp.SerialNumber)
}
func TestOcsp_ValidRequests(t *testing.T) { func TestOcsp_ValidRequests(t *testing.T) {
type caKeyConf struct { type caKeyConf struct {
keyType string keyType string
@ -588,7 +688,7 @@ func SendOcspRequest(t *testing.T, b *backend, s logical.Storage, getOrPost stri
} }
func sendOcspGetRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) { func sendOcspGetRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) {
urlEncoded := base64.StdEncoding.EncodeToString(ocspRequest) urlEncoded := url.QueryEscape(base64.StdEncoding.EncodeToString(ocspRequest))
return CBRead(b, s, "ocsp/"+urlEncoded) return CBRead(b, s, "ocsp/"+urlEncoded)
} }

3
changelog/18938.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
secrets/pki: OCSP GET request parameter was not being URL unescaped before processing.
```