Skip to content

Commit

Permalink
Add cert CRL support. (sonic-net#269)
Browse files Browse the repository at this point in the history
Add cert CRL support. 

#### Why I did it
Support certificate revocation list.

#### How I did it
Download CRL and verify cert with CRL. 

#### How to verify it
Manually test:

I1119 06:45:56.678454     139 server.go:201] Created Server on localhost:50052, read-only: true
I1119 06:45:56.678478     139 telemetry.go:465] Auth Modes: cert
I1119 06:45:56.678495     139 telemetry.go:466] Starting RPC server on address: localhost:50052
I1119 06:45:56.678532     139 telemetry.go:469] GNMI Server started serving
I1119 06:46:14.936024     139 clientCertAuth.go:183] Get Crl Urls for cert: []
I1119 06:46:14.936363     139 clientCertAuth.go:224] Cert does not contains and CRL distribution points
I1119 06:46:14.936375     139 server.go:278] authenticate user , roles [role1]
I1119 06:46:21.524943     139 clientCertAuth.go:183] Get Crl Urls for cert: [http://10.250.0.102:1234/crl]
I1119 06:46:21.526022     139 clientCertAuth.go:93] SearchCrlCache not found cache for url: http://10.250.0.102:1234/crl
I1119 06:46:21.526138     139 clientCertAuth.go:158] Download CRL start: http://10.250.0.102:1234/crl
I1119 06:46:21.533821     139 clientCertAuth.go:176] Download CRL: http://10.250.0.102:1234/crl successed
I1119 06:46:21.534318     139 clientCertAuth.go:66] CrlExpired expireTime: Wed Nov 20 06:46:21 2024, now: Tue Nov 19 06:46:21 2024
I1119 06:46:21.534337     139 clientCertAuth.go:211] CreateStaticCRLProvider add crl: http://10.250.0.102:1234/crl content: [...]
I1119 06:46:21.535269     139 clientCertAuth.go:244] VerifyCertCrl peer certificate revoked: no unrevoked chains found: map[2:1]
I1119 06:46:21.535289     139 clientCertAuth.go:149] [TELEMETRY-2] Failed to verify cert with CRL; rpc error: code = Unauthenticated desc = Peer certificate revoked


Add new UT.

#### Work item tracking
Microsoft ADO (number only): 27146924

#### Which release branch to backport (provide reason below if selected)

<!--
- Note we only backport fixes to a release branch, *not* features!
- Please also provide a reason for the backporting below.
- e.g.
- [x] 202006
-->

- [ ] 201811
- [ ] 201911
- [ ] 202006
- [ ] 202012
- [ ] 202106
- [ ] 202111

#### Description for the changelog
Add cert CRL support. 

#### Link to config_db schema for YANG module changes
<!--
Provide a link to config_db schema for the table for which YANG model
is defined
Link should point to correct section on https://github.com/Azure/SONiC/wiki/Configuration.
-->

#### A picture of a cute animal (not mandatory but encouraged)
  • Loading branch information
liuh-80 authored Nov 20, 2024
1 parent e0f0924 commit e6bc0e7
Show file tree
Hide file tree
Showing 9 changed files with 596 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ go-deps-clean:

sonic-gnmi: $(GO_DEPS)
# advancetls 1.0.0 release need following patch to build by go-1.19
# patch -d vendor -p0 < patches/0002-Fix-advance-tls-build-with-go-119.patch
patch -d vendor -p0 < patches/0002-Fix-advance-tls-build-with-go-119.patch
# build service first which depends on advancetls
ifeq ($(CROSS_BUILD_ENVIRON),y)
$(GO) build -o ${GOBIN}/telemetry -mod=vendor $(BLD_FLAGS) github.com/sonic-net/sonic-gnmi/telemetry
Expand Down
204 changes: 203 additions & 1 deletion gnmi_server/clientCertAuth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package gnmi

import (
"crypto/tls"
"crypto/x509"
"io"
"net/http"
"time"
"github.com/sonic-net/sonic-gnmi/common_utils"
"github.com/sonic-net/sonic-gnmi/swsscommon"
"github.com/golang/glog"
Expand All @@ -9,9 +14,103 @@ import (
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/grpc/security/advancedtls"
)

func ClientCertAuthenAndAuthor(ctx context.Context, serviceConfigTableName string) (context.Context, error) {
const DEFAULT_CRL_EXPIRE_DURATION time.Duration = 24 * 60* 60 * time.Second

type Crl struct {
thisUpdate time.Time
nextUpdate time.Time
crl []byte
}

// CRL content cache
var CrlCache map[string]*Crl = nil

// CRL content cache
var CrlDxpireDuration time.Duration = DEFAULT_CRL_EXPIRE_DURATION

func InitCrlCache() {
if CrlCache == nil {
CrlCache = make(map[string]*Crl)
}
}

func ReleaseCrlCache() {
for mapkey, _ := range(CrlCache) {
delete(CrlCache, mapkey)
}
}

func AppendCrlToCache(url string, rawCRL []byte) {
crl := new(Crl)
crl.thisUpdate = time.Now()
crl.nextUpdate = time.Now()
crl.crl = rawCRL

CrlCache[url] = crl
}

func GetCrlExpireDuration() time.Duration {
return CrlDxpireDuration
}

func SetCrlExpireDuration(duration time.Duration) {
CrlDxpireDuration = duration
}

func CrlExpired(crl *Crl) bool {
now := time.Now()
expireTime := crl.thisUpdate.Add(GetCrlExpireDuration())
glog.Infof("CrlExpired expireTime: %s, now: %s", expireTime.Format(time.ANSIC), now.Format(time.ANSIC))
// CRL expiresion policy follow the policy of Get-CRLFreshness command in following doc:
// https://learn.microsoft.com/en-us/archive/blogs/russellt/get-crlfreshness
// The policy are:
// 1. CRL expired when current time is after CRL expiresion time, which defined in "Next CRL Publish" extension.
// Because CRL cached in memory, GNMI support OnDemand CRL referesh by restart GNMI service.
return now.After(expireTime)
}

func CrlNeedUpdate(crl *Crl) bool {
now := time.Now()
glog.Infof("CrlNeedUpdate nextUpdate: %s, now: %s", crl.nextUpdate.Format(time.ANSIC), now.Format(time.ANSIC))
return now.After(crl.nextUpdate)
}

func RemoveExpiredCrl() {
for mapkey, crl := range(CrlCache) {
if CrlExpired(crl) {
glog.Infof("RemoveExpiredCrl key: %s", mapkey)
delete(CrlCache, mapkey)
}
}
}

func SearchCrlCache(url string) (bool, *Crl) {
crl, exist := CrlCache[url]
if !exist {
glog.Infof("SearchCrlCache not found cache for url: %s", url)
return false, nil
}

if CrlExpired(crl) {
glog.Infof("SearchCrlCache crl expired: %s", url)
delete(CrlCache, url)
return false, nil
}

if CrlNeedUpdate(crl) {
glog.Infof("SearchCrlCache crl need update: %s", url)
delete(CrlCache, url)
return false, nil
}

glog.Infof("SearchCrlCache found cache for url: %s", url)
return true, crl
}

func ClientCertAuthenAndAuthor(ctx context.Context, serviceConfigTableName string, enableCrl bool) (context.Context, error) {
rc, ctx := common_utils.GetContext(ctx)
p, ok := peer.FromContext(ctx)
if !ok {
Expand Down Expand Up @@ -44,9 +143,112 @@ func ClientCertAuthenAndAuthor(ctx context.Context, serviceConfigTableName strin
}
}

if enableCrl {
err := VerifyCertCrl(tlsAuth.State)
if err != nil {
glog.Infof("[%s] Failed to verify cert with CRL; %v", rc.ID, err)
return ctx, err
}
}

return ctx, nil
}

func TryDownload(url string) bool {
glog.Infof("Download CRL start: %s", url)
resp, err := http.Get(url)

if resp != nil {
defer resp.Body.Close()
}

if err != nil || resp.StatusCode != http.StatusOK {
glog.Infof("Download CRL: %s failed: %v", url, err)
return false
}

crlContent, err := io.ReadAll(resp.Body)
if err != nil {
glog.Infof("Download CRL: %s failed: %v", url, err)
return false
}

glog.Infof("Download CRL: %s successed", url)
AppendCrlToCache(url, crlContent)

return true
}

func GetCrlUrls(cert x509.Certificate) []string {
glog.Infof("Get Crl Urls for cert: %v", cert.CRLDistributionPoints)
return cert.CRLDistributionPoints
}

func DownloadNotCachedCrl(crlUrlArray []string) bool {
crlAvaliable := false
for _, crlUrl := range crlUrlArray{
exist, _ := SearchCrlCache(crlUrl)
if exist {
crlAvaliable = true
} else {
downloaded := TryDownload(crlUrl)
if downloaded {
crlAvaliable = true
}
}
}

return crlAvaliable
}

func CreateStaticCRLProvider() *advancedtls.StaticCRLProvider {
crlArray := make([][]byte, 1)
for mapkey, item := range(CrlCache) {
if CrlExpired(item) {
glog.Infof("CreateStaticCRLProvider remove expired crl: %s", mapkey)
delete(CrlCache, mapkey)
} else {
glog.Infof("CreateStaticCRLProvider add crl: %s content: %v", mapkey, item.crl)
crlArray = append(crlArray, item.crl)
}
}

return advancedtls.NewStaticCRLProvider(crlArray)
}

func VerifyCertCrl(tlsConnState tls.ConnectionState) error {
InitCrlCache()
// Check if any CRL already exist in local
crlUriArray := GetCrlUrls(*tlsConnState.VerifiedChains[0][0])
if len(crlUriArray) == 0 {
glog.Infof("Cert does not contains and CRL distribution points")
return nil
}

crlAvaliable := DownloadNotCachedCrl(crlUriArray)
if !crlAvaliable {
// Every certificate will contain multiple CRL distribution points.
// If all CRLs are not available, the certificate validation should be blocked.
glog.Infof("VerifyCertCrl can't download CRL and verify cert: %v", crlUriArray)
return status.Errorf(codes.Unauthenticated, "Can't download CRL and verify cert")
}

// Build CRL provider from cache and verify cert
crlProvider := CreateStaticCRLProvider()
err := advancedtls.CheckChainRevocation(tlsConnState.VerifiedChains, advancedtls.RevocationOptions{
DenyUndetermined: false,
CRLProvider: crlProvider,
})

if err != nil {
glog.Infof("VerifyCertCrl peer certificate revoked: %v", err.Error())
return status.Error(codes.Unauthenticated, "Peer certificate revoked")
}

glog.Infof("VerifyCertCrl verify cert passed: %v", crlUriArray)
return nil
}

func PopulateAuthStructByCommonName(certCommonName string, auth *common_utils.AuthInfo, serviceConfigTableName string) error {
if serviceConfigTableName == "" {
return status.Errorf(codes.Unauthenticated, "Service config table name should not be empty")
Expand Down
Loading

0 comments on commit e6bc0e7

Please sign in to comment.