Skip to content

Commit 7510083

Browse files
authored
feat: delta CRL (#247)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
1 parent b07b0ef commit 7510083

16 files changed

+1060
-140
lines changed

revocation/crl/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ import "errors"
1717

1818
// ErrCacheMiss is returned when a cache miss occurs.
1919
var ErrCacheMiss = errors.New("cache miss")
20+
21+
// errDeltaCRLNotFound is returned when a delta CRL is not found.
22+
var errDeltaCRLNotFound = errors.New("delta CRL not found")

revocation/crl/fetcher.go

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ package crl
1818
import (
1919
"context"
2020
"crypto/x509"
21+
"crypto/x509/pkix"
2122
"encoding/asn1"
2223
"errors"
2324
"fmt"
2425
"io"
2526
"net/http"
2627
"net/url"
2728
"time"
29+
30+
"github.com/notaryproject/notation-core-go/revocation/internal/x509util"
31+
"golang.org/x/crypto/cryptobyte"
32+
cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
2833
)
2934

3035
// oidFreshestCRL is the object identifier for the distribution point
@@ -84,9 +89,8 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) {
8489
if f.Cache != nil {
8590
bundle, err := f.Cache.Get(ctx, url)
8691
if err == nil {
87-
// check expiry
88-
nextUpdate := bundle.BaseCRL.NextUpdate
89-
if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) {
92+
// check expiry of base CRL and delta CRL
93+
if isEffective(bundle.BaseCRL) && (bundle.DeltaCRL == nil || isEffective(bundle.DeltaCRL)) {
9094
return bundle, nil
9195
}
9296
} else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError {
@@ -109,6 +113,11 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) {
109113
return bundle, nil
110114
}
111115

116+
// isEffective checks if the CRL is effective by checking the NextUpdate time.
117+
func isEffective(crl *x509.RevocationList) bool {
118+
return !crl.NextUpdate.IsZero() && !time.Now().After(crl.NextUpdate)
119+
}
120+
112121
// fetch downloads the CRL from the given URL.
113122
func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) {
114123
// fetch base CRL
@@ -117,19 +126,109 @@ func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) {
117126
return nil, err
118127
}
119128

120-
// check delta CRL
121-
// TODO: support delta CRL https://github.com/notaryproject/notation-core-go/issues/228
122-
for _, ext := range base.Extensions {
123-
if ext.Id.Equal(oidFreshestCRL) {
124-
return nil, errors.New("delta CRL is not supported")
125-
}
129+
// fetch delta CRL from base CRL extension
130+
deltaCRL, err := f.fetchDeltaCRL(ctx, base.Extensions)
131+
if err != nil && !errors.Is(err, errDeltaCRLNotFound) {
132+
return nil, err
126133
}
127134

128135
return &Bundle{
129-
BaseCRL: base,
136+
BaseCRL: base,
137+
DeltaCRL: deltaCRL,
130138
}, nil
131139
}
132140

141+
// fetchDeltaCRL fetches the delta CRL from the given extensions of base CRL.
142+
//
143+
// It returns errDeltaCRLNotFound if the delta CRL is not found.
144+
func (f *HTTPFetcher) fetchDeltaCRL(ctx context.Context, extensions []pkix.Extension) (*x509.RevocationList, error) {
145+
extension := x509util.FindExtensionByOID(extensions, oidFreshestCRL)
146+
if extension == nil {
147+
return nil, errDeltaCRLNotFound
148+
}
149+
150+
// RFC 5280, 4.2.1.15
151+
// id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 }
152+
//
153+
// FreshestCRL ::= CRLDistributionPoints
154+
urls, err := parseCRLDistributionPoint(extension.Value)
155+
if err != nil {
156+
return nil, fmt.Errorf("failed to parse Freshest CRL extension: %w", err)
157+
}
158+
if len(urls) == 0 {
159+
return nil, errDeltaCRLNotFound
160+
}
161+
162+
var (
163+
lastError error
164+
deltaCRL *x509.RevocationList
165+
)
166+
for _, cdpURL := range urls {
167+
// RFC 5280, 5.2.6
168+
// Delta CRLs from the base CRL have the same scope as the base
169+
// CRL, so the URLs are for redundancy and should be tried in
170+
// order until one succeeds.
171+
deltaCRL, lastError = fetchCRL(ctx, cdpURL, f.httpClient)
172+
if lastError == nil {
173+
return deltaCRL, nil
174+
}
175+
}
176+
return nil, lastError
177+
}
178+
179+
// parseCRLDistributionPoint parses the CRL extension and returns the CRL URLs
180+
//
181+
// value is the raw value of the CRL distribution point extension
182+
func parseCRLDistributionPoint(value []byte) ([]string, error) {
183+
var urls []string
184+
// borrowed from crypto/x509: https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/crypto/x509/parser.go;l=700-743
185+
//
186+
// RFC 5280, 4.2.1.13
187+
//
188+
// CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
189+
//
190+
// DistributionPoint ::= SEQUENCE {
191+
// distributionPoint [0] DistributionPointName OPTIONAL,
192+
// reasons [1] ReasonFlags OPTIONAL,
193+
// cRLIssuer [2] GeneralNames OPTIONAL }
194+
//
195+
// DistributionPointName ::= CHOICE {
196+
// fullName [0] GeneralNames,
197+
// nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
198+
val := cryptobyte.String(value)
199+
if !val.ReadASN1(&val, cbasn1.SEQUENCE) {
200+
return nil, errors.New("x509: invalid CRL distribution points")
201+
}
202+
for !val.Empty() {
203+
var dpDER cryptobyte.String
204+
if !val.ReadASN1(&dpDER, cbasn1.SEQUENCE) {
205+
return nil, errors.New("x509: invalid CRL distribution point")
206+
}
207+
var dpNameDER cryptobyte.String
208+
var dpNamePresent bool
209+
if !dpDER.ReadOptionalASN1(&dpNameDER, &dpNamePresent, cbasn1.Tag(0).Constructed().ContextSpecific()) {
210+
return nil, errors.New("x509: invalid CRL distribution point")
211+
}
212+
if !dpNamePresent {
213+
continue
214+
}
215+
if !dpNameDER.ReadASN1(&dpNameDER, cbasn1.Tag(0).Constructed().ContextSpecific()) {
216+
return nil, errors.New("x509: invalid CRL distribution point")
217+
}
218+
for !dpNameDER.Empty() {
219+
if !dpNameDER.PeekASN1Tag(cbasn1.Tag(6).ContextSpecific()) {
220+
break
221+
}
222+
var uri cryptobyte.String
223+
if !dpNameDER.ReadASN1(&uri, cbasn1.Tag(6).ContextSpecific()) {
224+
return nil, errors.New("x509: invalid CRL distribution point")
225+
}
226+
urls = append(urls, string(uri))
227+
}
228+
}
229+
return urls, nil
230+
}
231+
133232
func fetchCRL(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) {
134233
// validate URL
135234
parsedURL, err := url.Parse(crlURL)

0 commit comments

Comments
 (0)