Skip to content

Commit 9c042e6

Browse files
authored
Merge pull request #2217 from BishopFox/fix/cert-revoke
Improve operator cert revocation
2 parents 26abda1 + 62583ca commit 9c042e6

9 files changed

Lines changed: 745 additions & 19 deletions

File tree

go-tests.sh

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ set -u
2020
set -o pipefail
2121

2222
SKIP_GENERATE=0
23+
UNIT_ONLY=0
2324
for arg in "$@"; do
2425
if [ "$arg" = "--skip-generate" ]; then
2526
SKIP_GENERATE=1
2627
fi
28+
if [ "$arg" = "--unit-only" ]; then
29+
UNIT_ONLY=1
30+
SKIP_GENERATE=1
31+
fi
2732
done
2833

2934
echo "----------------------------------------------------------------"
@@ -219,8 +224,13 @@ done
219224

220225
## Server c2 (unit + e2e)
221226
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
222-
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
223-
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
227+
if [ "$UNIT_ONLY" -eq 1 ]; then
228+
echo
229+
echo "Skipping ./server/c2 e2e tests (--unit-only)"
230+
else
231+
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
232+
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
233+
fi
224234

225235
## Server generate
226236
if [ "$SKIP_GENERATE" -eq 0 ]; then
@@ -235,5 +245,9 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
235245
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
236246
else
237247
echo
238-
echo "Skipping ./server/generate tests (--skip-generate)"
248+
if [ "$UNIT_ONLY" -eq 1 ]; then
249+
echo "Skipping ./server/generate tests (--unit-only)"
250+
else
251+
echo "Skipping ./server/generate tests (--skip-generate)"
252+
fi
239253
fi

server/certs/operators.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package certs
2121
import (
2222
"crypto/x509"
2323
"encoding/pem"
24+
"errors"
2425
"fmt"
2526

2627
"github.com/bishopfox/sliver/server/db"
@@ -35,6 +36,17 @@ const (
3536
serverNamespace = "server" // Operator servers
3637
)
3738

39+
var (
40+
// ErrOperatorClientCertificateNotFound indicates that the presented operator
41+
// client certificate is no longer trusted because it is not present in the
42+
// certificate store.
43+
ErrOperatorClientCertificateNotFound = errors.New("operator client certificate not found in database")
44+
45+
// ErrInvalidOperatorClientCertificate indicates that the presented
46+
// certificate is not shaped like an operator client leaf certificate.
47+
ErrInvalidOperatorClientCertificate = errors.New("invalid operator client certificate")
48+
)
49+
3850
// OperatorClientGenerateCertificate - Generate a certificate signed with a given CA
3951
func OperatorClientGenerateCertificate(operator string) ([]byte, []byte, error) {
4052
cert, key := GenerateECCCertificate(OperatorCA, operator, false, true, true)
@@ -52,6 +64,52 @@ func OperatorClientRemoveCertificate(operator string) error {
5264
return RemoveCertificate(OperatorCA, ECCKey, fmt.Sprintf("%s.%s", clientNamespace, operator))
5365
}
5466

67+
// ValidateOperatorClientCertificate ensures that the presented operator client
68+
// certificate is still present in the database. A valid chain alone is not
69+
// enough; the exact leaf certificate must still exist in storage.
70+
func ValidateOperatorClientCertificate(peerCertificates []*x509.Certificate) error {
71+
if len(peerCertificates) == 0 || peerCertificates[0] == nil {
72+
return ErrInvalidOperatorClientCertificate
73+
}
74+
75+
leaf := peerCertificates[0]
76+
if leaf.IsCA || leaf.Subject.CommonName == "" || !hasExtKeyUsage(leaf, x509.ExtKeyUsageClientAuth) {
77+
return ErrInvalidOperatorClientCertificate
78+
}
79+
80+
pemBytes := pem.EncodeToMemory(&pem.Block{
81+
Type: "CERTIFICATE",
82+
Bytes: leaf.Raw,
83+
})
84+
if len(pemBytes) == 0 {
85+
return ErrInvalidOperatorClientCertificate
86+
}
87+
88+
record := &models.Certificate{}
89+
result := db.Session().Select("id").Where(&models.Certificate{
90+
CommonName: fmt.Sprintf("%s.%s", clientNamespace, leaf.Subject.CommonName),
91+
CAType: OperatorCA,
92+
KeyType: ECCKey,
93+
CertificatePEM: string(pemBytes),
94+
}).First(record)
95+
if result.Error == nil {
96+
return nil
97+
}
98+
if errors.Is(result.Error, db.ErrRecordNotFound) {
99+
return ErrOperatorClientCertificateNotFound
100+
}
101+
return result.Error
102+
}
103+
104+
func hasExtKeyUsage(cert *x509.Certificate, usage x509.ExtKeyUsage) bool {
105+
for _, extKeyUsage := range cert.ExtKeyUsage {
106+
if extKeyUsage == usage {
107+
return true
108+
}
109+
}
110+
return false
111+
}
112+
55113
// OperatorServerGetCertificate - Helper function to fetch a server cert
56114
func OperatorServerGetCertificate(hostname string) ([]byte, []byte, error) {
57115
return GetECCCertificate(OperatorCA, fmt.Sprintf("%s.%s", serverNamespace, hostname))
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package certs
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"errors"
7+
"fmt"
8+
"testing"
9+
"time"
10+
11+
"github.com/bishopfox/sliver/server/db"
12+
"github.com/bishopfox/sliver/server/db/models"
13+
)
14+
15+
func TestValidateOperatorClientCertificateAdversarial(t *testing.T) {
16+
SetupCAs()
17+
18+
t.Run("accepts stored operator client leaf", func(t *testing.T) {
19+
operatorName := uniqueOperatorCertificateName(t, "stored")
20+
certPEM, _, err := OperatorClientGenerateCertificate(operatorName)
21+
if err != nil {
22+
t.Fatalf("generate operator client certificate: %v", err)
23+
}
24+
25+
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, certPEM)})
26+
if err != nil {
27+
t.Fatalf("expected stored operator client certificate to be accepted, got %v", err)
28+
}
29+
})
30+
31+
t.Run("rejects valid stored certificate hidden behind an unstored first certificate", func(t *testing.T) {
32+
operatorName := uniqueOperatorCertificateName(t, "hidden")
33+
storedCertPEM, _, err := OperatorClientGenerateCertificate(operatorName)
34+
if err != nil {
35+
t.Fatalf("generate stored operator certificate: %v", err)
36+
}
37+
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
38+
39+
err = ValidateOperatorClientCertificate([]*x509.Certificate{
40+
mustParseCertificate(t, rogueCertPEM),
41+
mustParseCertificate(t, storedCertPEM),
42+
})
43+
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
44+
t.Fatalf("expected first unstored certificate to be rejected, got %v", err)
45+
}
46+
})
47+
48+
t.Run("rejects same-common-name different-leaf certificate", func(t *testing.T) {
49+
operatorName := uniqueOperatorCertificateName(t, "same-cn")
50+
_, _, err := OperatorClientGenerateCertificate(operatorName)
51+
if err != nil {
52+
t.Fatalf("generate stored operator certificate: %v", err)
53+
}
54+
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
55+
56+
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, rogueCertPEM)})
57+
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
58+
t.Fatalf("expected rotated or forged same-CN leaf to be rejected, got %v", err)
59+
}
60+
})
61+
62+
t.Run("rejects stored operator server certificate", func(t *testing.T) {
63+
hostName := uniqueOperatorCertificateName(t, "server")
64+
serverCertPEM, _, err := OperatorServerGenerateCertificate(hostName)
65+
if err != nil {
66+
t.Fatalf("generate operator server certificate: %v", err)
67+
}
68+
69+
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, serverCertPEM)})
70+
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
71+
t.Fatalf("expected stored operator server certificate to be rejected, got %v", err)
72+
}
73+
})
74+
75+
t.Run("rejects operator CA certificate even if inserted into the certificates table", func(t *testing.T) {
76+
caCertPEM, caKeyPEM, err := GetCertificateAuthorityPEM(OperatorCA)
77+
if err != nil {
78+
t.Fatalf("get operator CA: %v", err)
79+
}
80+
caCert := mustParseCertificate(t, caCertPEM)
81+
82+
record := &models.Certificate{
83+
CommonName: fmt.Sprintf("%s.%s", clientNamespace, caCert.Subject.CommonName),
84+
CAType: OperatorCA,
85+
KeyType: ECCKey,
86+
CertificatePEM: string(caCertPEM),
87+
PrivateKeyPEM: string(caKeyPEM),
88+
}
89+
if err := db.Session().Create(record).Error; err != nil {
90+
t.Fatalf("insert CA cert into certificates table: %v", err)
91+
}
92+
93+
err = ValidateOperatorClientCertificate([]*x509.Certificate{caCert})
94+
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
95+
t.Fatalf("expected CA certificate to be rejected, got %v", err)
96+
}
97+
})
98+
}
99+
100+
func mustParseCertificate(t *testing.T, certPEM []byte) *x509.Certificate {
101+
t.Helper()
102+
103+
block, _ := pem.Decode(certPEM)
104+
if block == nil {
105+
t.Fatal("failed to decode certificate PEM")
106+
}
107+
cert, err := x509.ParseCertificate(block.Bytes)
108+
if err != nil {
109+
t.Fatalf("parse x509 certificate: %v", err)
110+
}
111+
return cert
112+
}
113+
114+
func uniqueOperatorCertificateName(t *testing.T, prefix string) string {
115+
t.Helper()
116+
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
117+
}

server/console/console-admin.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,18 +218,9 @@ func kickOperatorCmd(cmd *cobra.Command, args []string) {
218218
}
219219

220220
fmt.Printf(Info+"Removing auth token(s) for %s, please wait ... \n", operator)
221-
err = db.Session().Where(&models.Operator{
222-
Name: operator,
223-
}).Delete(&models.Operator{}).Error
224-
if err != nil {
225-
fmt.Printf(Warn+"Failed to remove operator %s: %v\n", operator, err)
226-
return
227-
}
228-
transport.ClearTokenCache()
229-
fmt.Printf(Info+"Removing client certificate(s) for %s, please wait ... \n", operator)
230-
err = certs.OperatorClientRemoveCertificate(operator)
221+
err = kickOperator(operator)
231222
if err != nil {
232-
fmt.Printf(Warn+"Failed to remove the operator certificate: %v \n", err)
223+
fmt.Printf(Warn+"Failed to kick operator %s: %v\n", operator, err)
233224
return
234225
}
235226
fmt.Printf(Info+"Operator %s has been kicked out.\n", operator)
@@ -242,6 +233,33 @@ func shouldPromptKickOperator(cmd *cobra.Command, args []string) bool {
242233
return cmd.Flags().NFlag() == 0
243234
}
244235

236+
func removeOperator(operator string) error {
237+
err := db.Session().Where(&models.Operator{
238+
Name: operator,
239+
}).Delete(&models.Operator{}).Error
240+
if err != nil {
241+
return err
242+
}
243+
transport.ClearTokenCache()
244+
return nil
245+
}
246+
247+
func revokeOperatorClientCertificate(operator string) error {
248+
return certs.OperatorClientRemoveCertificate(operator)
249+
}
250+
251+
func closeOperatorStreams(operator string) {
252+
transport.CloseOperatorStreams(operator)
253+
}
254+
255+
func kickOperator(operator string) error {
256+
if err := removeOperator(operator); err != nil {
257+
return err
258+
}
259+
defer closeOperatorStreams(operator)
260+
return revokeOperatorClientCertificate(operator)
261+
}
262+
245263
func operatorNames() ([]string, error) {
246264
operators, err := db.OperatorAll()
247265
if err != nil {

0 commit comments

Comments
 (0)