@@ -18,13 +18,18 @@ package crl
1818import (
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.
113122func (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+
133232func fetchCRL (ctx context.Context , crlURL string , client * http.Client ) (* x509.RevocationList , error ) {
134233 // validate URL
135234 parsedURL , err := url .Parse (crlURL )
0 commit comments