Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delta CRL #247

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 114 additions & 11 deletions revocation/crl/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"

"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
)

// oidFreshestCRL is the object identifier for the distribution point
Expand All @@ -44,6 +48,15 @@
Fetch(ctx context.Context, url string) (*Bundle, error)
}

// FetcherWithCertificateExtensions is an interface that specifies methods used
// for fetching CRL from the given URL with certificate extensions to identify
// the Freshest CRL extension (Delta CRL) from certificate.
type FetcherWithCertificateExtensions interface {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There 2 sources of Delta CRL URL:

  1. certificate extension
  2. CRL extension

This interface enable user to pass a certificate extension to the fetcher for accessing the delta CRL.

// FetchWithCertificateExtensions retrieves the CRL from the given URL with
// certificate extensions.
FetchWithCertificateExtensions(ctx context.Context, url string, certificateExtensions *[]pkix.Extension) (*Bundle, error)
}

// HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL
type HTTPFetcher struct {
// Cache stores fetched CRLs and reuses them until the CRL reaches the
Expand Down Expand Up @@ -77,24 +90,28 @@
// (e.g. cache miss), it will download the CRL from the URL and store it to the
// cache.
func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) {
return f.FetchWithCertificateExtensions(ctx, url, nil)
}

func (f *HTTPFetcher) FetchWithCertificateExtensions(ctx context.Context, url string, certificateExtensions *[]pkix.Extension) (*Bundle, error) {
if url == "" {
return nil, errors.New("CRL URL cannot be empty")
}

if f.Cache != nil {
bundle, err := f.Cache.Get(ctx, url)
if err == nil {
// check expiry
nextUpdate := bundle.BaseCRL.NextUpdate
if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) {
// check expiry of base CRL and delta CRL
if (bundle.BaseCRL != nil && isEffective(bundle.BaseCRL)) &&
(bundle.DeltaCRL == nil || isEffective(bundle.DeltaCRL)) {
return bundle, nil
}
} else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError {
return nil, fmt.Errorf("failed to retrieve CRL from cache: %w", err)
}
}

bundle, err := f.fetch(ctx, url)
bundle, err := f.fetch(ctx, url, certificateExtensions)
if err != nil {
return nil, fmt.Errorf("failed to retrieve CRL: %w", err)
}
Expand All @@ -109,27 +126,113 @@
return bundle, nil
}

// isEffective checks if the CRL is effective by checking the NextUpdate time.
func isEffective(crl *x509.RevocationList) bool {
return !crl.NextUpdate.IsZero() && !time.Now().After(crl.NextUpdate)
}

// fetch downloads the CRL from the given URL.
func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) {
func (f *HTTPFetcher) fetch(ctx context.Context, url string, certificateExtensions *[]pkix.Extension) (*Bundle, error) {
// fetch base CRL
base, err := fetchCRL(ctx, url, f.httpClient)
if err != nil {
return nil, err
}

// check delta CRL
// TODO: support delta CRL https://github.com/notaryproject/notation-core-go/issues/228
for _, ext := range base.Extensions {
if ext.Id.Equal(oidFreshestCRL) {
return nil, errors.New("delta CRL is not supported")
// fetch delta CRL from CRL extension
deltaCRL, err := f.processDeltaCRL(&base.Extensions)
if err != nil {
return nil, err
}
if certificateExtensions != nil && deltaCRL == nil {
// fallback to fetch delta CRL from certificate extension
deltaCRL, err = f.processDeltaCRL(certificateExtensions)
if err != nil {
return nil, err
}
}

return &Bundle{
BaseCRL: base,
BaseCRL: base,
DeltaCRL: deltaCRL,
}, nil
}

func (f *HTTPFetcher) processDeltaCRL(extensions *[]pkix.Extension) (*x509.RevocationList, error) {
for _, ext := range *extensions {
if ext.Id.Equal(oidFreshestCRL) {
cdp, err := parseFreshestCRL(ext)
if err != nil {
return nil, fmt.Errorf("failed to parse Freshest CRL extension: %w", err)
}
if len(cdp) == 0 {
return nil, nil
}
if len(cdp) > 1 {
// to simplify the implementation, we only support one
// Freshest CRL URL for now which is the common case
return nil, errors.New("multiple Freshest CRL distribution points are not supported")
}
return fetchCRL(context.Background(), cdp[0], f.httpClient)
}
}
return nil, nil
}

// parseFreshestCRL parses the Freshest CRL extension and returns the CRL URLs
func parseFreshestCRL(ext pkix.Extension) ([]string, error) {
var cdp []string
// RFC 5280, 4.2.1.15
// id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 }
//
// FreshestCRL ::= CRLDistributionPoints
//
// RFC 5280, 4.2.1.13
//
// CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
//
// DistributionPoint ::= SEQUENCE {
// distributionPoint [0] DistributionPointName OPTIONAL,
// reasons [1] ReasonFlags OPTIONAL,
// cRLIssuer [2] GeneralNames OPTIONAL }
//
// DistributionPointName ::= CHOICE {
// fullName [0] GeneralNames,
// nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
val := cryptobyte.String(ext.Value)
if !val.ReadASN1(&val, cryptobyte_asn1.SEQUENCE) {
return nil, errors.New("x509: invalid CRL distribution points")
}
for !val.Empty() {
var dpDER cryptobyte.String
if !val.ReadASN1(&dpDER, cryptobyte_asn1.SEQUENCE) {
return nil, errors.New("x509: invalid CRL distribution point")
}

Check warning on line 210 in revocation/crl/fetcher.go

View check run for this annotation

Codecov / codecov/patch

revocation/crl/fetcher.go#L209-L210

Added lines #L209 - L210 were not covered by tests
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those lines can hardly be triggered.

var dpNameDER cryptobyte.String
var dpNamePresent bool
if !dpDER.ReadOptionalASN1(&dpNameDER, &dpNamePresent, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) {
return nil, errors.New("x509: invalid CRL distribution point")
}

Check warning on line 215 in revocation/crl/fetcher.go

View check run for this annotation

Codecov / codecov/patch

revocation/crl/fetcher.go#L214-L215

Added lines #L214 - L215 were not covered by tests
if !dpNamePresent {
continue
}
if !dpNameDER.ReadASN1(&dpNameDER, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) {
return nil, errors.New("x509: invalid CRL distribution point")
}
for !dpNameDER.Empty() {
if !dpNameDER.PeekASN1Tag(cryptobyte_asn1.Tag(6).ContextSpecific()) {
break
}
var uri cryptobyte.String
if !dpNameDER.ReadASN1(&uri, cryptobyte_asn1.Tag(6).ContextSpecific()) {
return nil, errors.New("x509: invalid CRL distribution point")
}

Check warning on line 229 in revocation/crl/fetcher.go

View check run for this annotation

Codecov / codecov/patch

revocation/crl/fetcher.go#L228-L229

Added lines #L228 - L229 were not covered by tests
cdp = append(cdp, string(uri))
}
}
return cdp, nil
}

func fetchCRL(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) {
// validate URL
parsedURL, err := url.Parse(crlURL)
Expand Down
Loading
Loading