diff --git a/repo/config.go b/repo/config.go index 5da2b4d0..9bf17412 100644 --- a/repo/config.go +++ b/repo/config.go @@ -6,6 +6,8 @@ import ( "path/filepath" enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/ndn" + sec "github.com/named-data/ndnd/std/security" ) type Config struct { @@ -59,6 +61,13 @@ func (c *Config) TrustAnchorNames() []enc.Name { return res } +func (c *Config) certExpiredCallback() ndn.CertExpiredCallback { + if c.IgnoreValidity { + return sec.IgnoreExpiredCert + } + return sec.ValidateAtSignatureTime +} + // (AI GENERATED DESCRIPTION): Returns a new Config with default placeholder values: empty Name and StorageDir strings, and a nil NameN slice. func DefaultConfig() *Config { return &Config{ diff --git a/repo/repo_mgmt.go b/repo/repo_mgmt.go index 625790cc..bed7569b 100644 --- a/repo/repo_mgmt.go +++ b/repo/repo_mgmt.go @@ -12,7 +12,6 @@ import ( "github.com/named-data/ndnd/std/object" sec "github.com/named-data/ndnd/std/security" "github.com/named-data/ndnd/std/security/trust_schema" - "github.com/named-data/ndnd/std/types/optional" ) // (AI GENERATED DESCRIPTION): Parses a repository management command from the received wire and dispatches it to the sync‑join handler if present, otherwise logs a warning about an unknown command. @@ -197,10 +196,9 @@ func (r *Repo) fetchSecurityConfig(name enc.Name) (*tlv.SecurityConfigObject, er // Repo should validate this as normal command r.client.ConsumeExt(ndn.ConsumeExtArgs{ - Name: name, - TryStore: true, - UseSignatureTime: optional.Some(true), - IgnoreValidity: optional.Some(r.config.IgnoreValidity), + Name: name, + TryStore: true, + OnCertExpired: r.config.certExpiredCallback(), Callback: func(state ndn.ConsumeState) { wire = append(wire, state.Content()...) if state.Error() != nil { diff --git a/repo/repo_svs.go b/repo/repo_svs.go index 8ecd6a78..343743ab 100644 --- a/repo/repo_svs.go +++ b/repo/repo_svs.go @@ -11,7 +11,6 @@ import ( spec "github.com/named-data/ndnd/std/ndn/spec_2022" "github.com/named-data/ndnd/std/ndn/svs_ps" ndn_sync "github.com/named-data/ndnd/std/sync" - "github.com/named-data/ndnd/std/types/optional" ) type RepoSvs struct { @@ -54,11 +53,10 @@ func (r *RepoSvs) Start() (err error) { } snapshot = &ndn_sync.SnapshotNodeHistory{ - Client: r.client, - Threshold: r.cmd.HistorySnapshot.Threshold, - IsRepo: true, - UseSignatureTime: optional.Some(true), - IgnoreValidity: optional.Some(r.config.IgnoreValidity), + Client: r.client, + Threshold: r.cmd.HistorySnapshot.Threshold, + IsRepo: true, + OnCertExpired: r.config.certExpiredCallback(), } } @@ -79,8 +77,7 @@ func (r *RepoSvs) Start() (err error) { SuppressionPeriod: 500 * time.Millisecond, PeriodicTimeout: 365 * 24 * time.Hour, // basically never Passive: true, - UseSignatureTime: optional.Some(true), - IgnoreValidity: optional.Some(r.config.IgnoreValidity), + OnCertExpired: r.config.certExpiredCallback(), }, Snapshot: snapshot, MulticastPrefix: multicastPrefix, diff --git a/std/ndn/client.go b/std/ndn/client.go index 9a509e02..feb01242 100644 --- a/std/ndn/client.go +++ b/std/ndn/client.go @@ -129,10 +129,9 @@ type ConsumeExtArgs struct { OnProgress func(status ConsumeState) // NoMetadata disables fetching RDR metadata (advanced usage). NoMetadata bool - // UseSignatureTime checks validity period using signature time - UseSignatureTime optional.Optional[bool] - // IgnoreValidity ignores validity period in the validation chain - IgnoreValidity optional.Optional[bool] + // OnCertExpired decides whether an expired certificate may be used. + // A nil callback rejects expired certificates. + OnCertExpired CertExpiredCallback } // ExpressRArgs are the arguments for the express retry API. @@ -169,10 +168,9 @@ type ValidateExtArgs struct { CertNextHop optional.Optional[uint64] // UseDataNameFwHint overrides trust config option. UseDataNameFwHint optional.Optional[bool] - // UseSignatureTime checks validity with signature time - UseSignatureTime optional.Optional[bool] - // IgnoreValidity ignores validity period in the validation chain - IgnoreValidity optional.Optional[bool] + // OnCertExpired decides whether an expired certificate may be used. + // A nil callback rejects expired certificates. + OnCertExpired CertExpiredCallback } // Announcement are the arguments for the announce prefix API. diff --git a/std/ndn/engine.go b/std/ndn/engine.go index ec242475..68778b9b 100644 --- a/std/ndn/engine.go +++ b/std/ndn/engine.go @@ -89,10 +89,6 @@ type ExpressCallbackArgs struct { // IsLocal indicates if a local copy of the Data was found. // e.g. returned by ExpressR when used with TryStore. IsLocal bool - // UseSignatureTime checks validity with signature time - UseSignatureTime optional.Optional[bool] - // IgnoreValidity ignores validity period in the validation chain - IgnoreValidity optional.Optional[bool] } // InterestHandler represents the callback function for an Interest handler. diff --git a/std/ndn/security.go b/std/ndn/security.go index 21a45fae..7ce9ac8c 100644 --- a/std/ndn/security.go +++ b/std/ndn/security.go @@ -51,6 +51,28 @@ type Signer interface { // Create a go routine for time consuming jobs. type SigChecker func(name enc.Name, sigCovered enc.Wire, sig Signature) bool +// CertExpiredCallbackArgs are the arguments passed to CertExpiredCallback. +type CertExpiredCallbackArgs struct { + // Data is the packet whose validation depends on Cert's validity. + Data Data + // Cert is the certificate or cross-schema packet authorizing Data. + Cert Data +} + +// CertExpiredCallback decides whether a validation relation involving an +// expired validity period may be used. For a Data <- Cert relation, it is +// called when Cert is expired or when expired Data is validated through Cert. +// Call complete with nil to continue validation or an error to reject it. +// Acceptance is authoritative: a successful validation may use the relation to +// establish trust that remains available to later validations. +// complete may be called synchronously or asynchronously, but must be called +// exactly once. The callback itself should return promptly without blocking +// the validation goroutine. +// An expired certificate in a chain may cause one call for Data <- certificate +// and another for certificate <- upstream signer. +// Separate validation operations may invoke the callback concurrently. +type CertExpiredCallback func(args CertExpiredCallbackArgs, complete func(error)) + // KeyChain is the interface of a keychain. // Note that Keychains are not thread-safe, and the owner should provide a lock. type KeyChain interface { diff --git a/std/object/client_consume.go b/std/object/client_consume.go index 64ba3e3e..5d764ad4 100644 --- a/std/object/client_consume.go +++ b/std/object/client_consume.go @@ -64,7 +64,7 @@ func (c *Client) consumeObject(state *ConsumeState) { // if metadata fetching is disabled, just attempt to fetch one segment // with the prefix, then get the versioned name from the segment. if state.args.NoMetadata { - c.fetchDataByPrefix(name, state.args.TryStore, state.args.UseSignatureTime.GetOr(false), state.args.IgnoreValidity.GetOr(false), + c.fetchDataByPrefix(name, state.args.TryStore, state.args.OnCertExpired, func(data ndn.Data, err error) { if err != nil { state.finalizeError(err) @@ -81,7 +81,7 @@ func (c *Client) consumeObject(state *ConsumeState) { } // fetch RDR metadata for this object - c.fetchMetadata(name, state.args.TryStore, state.args.UseSignatureTime.GetOr(false), state.args.IgnoreValidity.GetOr(false), + c.fetchMetadata(name, state.args.TryStore, state.args.OnCertExpired, func(meta *rdr.MetaData, err error) { if err != nil { state.finalizeError(err) @@ -107,8 +107,7 @@ func (c *Client) consumeObjectWithMeta(state *ConsumeState, meta *rdr.MetaData) func (c *Client) fetchMetadata( name enc.Name, tryStore bool, - useSignatureTime bool, - ignoreValidity bool, + onCertExpired ndn.CertExpiredCallback, callback func(meta *rdr.MetaData, err error), ) { log.Debug(c, "Fetching object metadata", "name", name) @@ -132,10 +131,9 @@ func (c *Client) fetchMetadata( return } c.ValidateExt(ndn.ValidateExtArgs{ - Data: args.Data, - SigCovered: args.SigCovered, - UseSignatureTime: optional.Some(useSignatureTime), - IgnoreValidity: optional.Some(ignoreValidity), + Data: args.Data, + SigCovered: args.SigCovered, + OnCertExpired: onCertExpired, Callback: func(valid bool, err error) { // validate with trust config if !valid { @@ -164,8 +162,7 @@ func (c *Client) fetchMetadata( func (c *Client) fetchDataByPrefix( name enc.Name, tryStore bool, - useSignatureTime bool, - ignoreValidity bool, + onCertExpired ndn.CertExpiredCallback, callback func(data ndn.Data, err error), ) { log.Debug(c, "Fetching data with prefix", "name", name) @@ -189,10 +186,9 @@ func (c *Client) fetchDataByPrefix( return } c.ValidateExt(ndn.ValidateExtArgs{ - Data: args.Data, - SigCovered: args.SigCovered, - UseSignatureTime: optional.Some(useSignatureTime), - IgnoreValidity: optional.Some(ignoreValidity), + Data: args.Data, + SigCovered: args.SigCovered, + OnCertExpired: onCertExpired, Callback: func(valid bool, err error) { if !valid { callback(nil, fmt.Errorf("%w: validate by prefix failed: %w", ndn.ErrSecurity, err)) diff --git a/std/object/client_consume_seg.go b/std/object/client_consume_seg.go index 8fa9440e..9191aa95 100644 --- a/std/object/client_consume_seg.go +++ b/std/object/client_consume_seg.go @@ -286,10 +286,9 @@ func (s *rrSegFetcher) handleResult(args ndn.ExpressCallbackArgs, state *Consume // The notable exception here is when there is a timeout, which has a separate goroutine. func (s *rrSegFetcher) handleData(args ndn.ExpressCallbackArgs, state *ConsumeState) { s.client.ValidateExt(ndn.ValidateExtArgs{ - Data: args.Data, - SigCovered: args.SigCovered, - UseSignatureTime: state.args.UseSignatureTime, - IgnoreValidity: state.args.IgnoreValidity, + Data: args.Data, + SigCovered: args.SigCovered, + OnCertExpired: state.args.OnCertExpired, Callback: func(valid bool, err error) { if !valid { state.finalizeError(fmt.Errorf("%w: validate seg failed: %w", ndn.ErrSecurity, err)) diff --git a/std/object/client_trust.go b/std/object/client_trust.go index cadb3d7d..5f8a333e 100644 --- a/std/object/client_trust.go +++ b/std/object/client_trust.go @@ -46,8 +46,7 @@ func (c *Client) ValidateExt(args ndn.ValidateExtArgs) { Callback: args.Callback, OverrideName: overrideName, UseDataNameFwHint: args.UseDataNameFwHint, - UseSignatureTime: args.UseSignatureTime, - IgnoreValidity: args.IgnoreValidity, + OnCertExpired: c.wrapOnCertExpired(args.OnCertExpired), Fetch: func(name enc.Name, config *ndn.InterestConfig, callback ndn.ExpressCallbackFunc) { config.NextHopId = args.CertNextHop c.ExpressR(ndn.ExpressRArgs{ @@ -61,6 +60,20 @@ func (c *Client) ValidateExt(args ndn.ValidateExtArgs) { }) } +// wrapOnCertExpired ensures validation resumes on the engine goroutine. +func (c *Client) wrapOnCertExpired(appCallback ndn.CertExpiredCallback) ndn.CertExpiredCallback { + if appCallback == nil { + return nil + } + return func(args ndn.CertExpiredCallbackArgs, resumeValidation func(error)) { + appCallback(args, func(err error) { + c.engine.Post(func() { + resumeValidation(err) + }) + }) + } +} + // SetTrustSchema replaces the client's trust schema at runtime. // No-op if the client does not have a trust config or if schema is nil. func (c *Client) SetTrustSchema(schema ndn.TrustSchema) { diff --git a/std/security/cert_cache.go b/std/security/cert_cache.go index 7b476784..8fb2e9c7 100644 --- a/std/security/cert_cache.go +++ b/std/security/cert_cache.go @@ -17,8 +17,9 @@ type CertCache struct { } type certCacheEntry struct { - data ndn.Data - expiry time.Time + data ndn.Data + sigCovered enc.Wire + expiry time.Time } // CertListCache stores validated CertList Data packets keyed by prefix and full name. @@ -68,30 +69,42 @@ func NewCertCache() *CertCache { // Get retrieves a certificate from the cache. // The name can be either the certificate name or the key locator. -// If the cert expires in less than 5 minutes, it is considered stale. +// Entries are retained until five minutes after certificate expiry. func (cc *CertCache) Get(name enc.Name) (ndn.Data, bool) { + entry, ok := cc.get(name) + return entry.data, ok +} + +// get retrieves the certificate and its signature verification evidence. +func (cc *CertCache) get(name enc.Name) (certCacheEntry, bool) { str := name.TlvStr() if v, ok := cc.cache.Load(str); ok { entry := v.(certCacheEntry) if entry.expiry.Add(5 * time.Minute).After(time.Now()) { - return entry.data, true + return entry, true } else { cc.cache.Delete(str) } } - return nil, false + return certCacheEntry{}, false } -// Put stores a certificate in the cache +// Put stores certificate data without signature verification evidence. func (cc *CertCache) Put(cert ndn.Data) { + cc.put(cert, nil) +} + +// put stores a certificate with the wire covered by its signature. +func (cc *CertCache) put(cert ndn.Data, sigCovered enc.Wire) { _, expiry := cert.Signature().Validity() if !expiry.IsSet() { return // huh? } entry := certCacheEntry{ - data: cert, - expiry: expiry.Unwrap(), + data: cert, + sigCovered: sigCovered, + expiry: expiry.Unwrap(), } // Store the certificate by its own name diff --git a/std/security/certificate.go b/std/security/certificate.go index 330e33a0..c638ac61 100644 --- a/std/security/certificate.go +++ b/std/security/certificate.go @@ -116,6 +116,26 @@ func CertIsExpired(cert ndn.Data) bool { return false } +// RejectExpiredCert rejects an expired certificate. +func RejectExpiredCert(args ndn.CertExpiredCallbackArgs, complete func(error)) { + complete(fmt.Errorf("certificate is expired: %s", args.Cert.Name())) +} + +// IgnoreExpiredCert accepts an expired certificate without additional checks. +func IgnoreExpiredCert(_ ndn.CertExpiredCallbackArgs, complete func(error)) { + complete(nil) +} + +// ValidateAtSignatureTime accepts an expired validation chain when each +// affected Data packet was signed during its Cert's validity period. +func ValidateAtSignatureTime(args ndn.CertExpiredCallbackArgs, complete func(error)) { + if ValidateSigTime(args.Data, args.Cert) { + complete(nil) + return + } + complete(fmt.Errorf("data not signed during validity period: %s", args.Cert.Name())) +} + // getPubKey gets the public key from an NDN data. // returns [public key, key name, error]. func getPubKey(data ndn.Data) ([]byte, enc.Name, error) { diff --git a/std/security/trust_config.go b/std/security/trust_config.go index 34c4f54e..7504b31b 100644 --- a/std/security/trust_config.go +++ b/std/security/trust_config.go @@ -1,6 +1,7 @@ package security import ( + "bytes" "fmt" "sync" @@ -25,8 +26,8 @@ type TrustConfig struct { // roots are the full names of the trust anchors. roots []enc.Name - // certCache is the certificate memcache. - // Everything in here is validated, fresh and passes the schema. + // certCache stores certificate data and its signature-covered wire. + // Cache hits are revalidated unless the certificate is a trust anchor. certCache *CertCache // certListCache stores validated CertLists. @@ -63,11 +64,11 @@ func NewTrustConfig(keyChain ndn.KeyChain, schema ndn.TrustSchema, roots []enc.N if certBytes, _ := keyChain.Store().Get(root, false); len(certBytes) == 0 { return nil, fmt.Errorf("trust anchor not found in keychain: %s", root) } else { - certData, _, err := spec.Spec{}.ReadData(enc.NewBufferView(certBytes)) + certData, certSigCov, err := spec.Spec{}.ReadData(enc.NewBufferView(certBytes)) if err != nil { return nil, fmt.Errorf("failed to parse trust anchor %s: %w", root, err) } - certCache.Put(certData) + certCache.put(certData, certSigCov) } } @@ -120,14 +121,19 @@ type TrustConfigValidateArgs struct { Callback func(bool, error) // OverrideName is an override for the data name (advanced usage). OverrideName enc.Name - // ignore ValidityPeriod in the validation chain - IgnoreValidity optional.Optional[bool] + // OnCertExpired decides whether an expired certificate may be used. + // A nil callback rejects expired certificates. + OnCertExpired ndn.CertExpiredCallback // origDataName is the original data name being verified. origDataName enc.Name + // crossSchemaExpired indicates Data is an expired cross-schema packet whose + // use by the original packet was accepted by the expiry policy. + crossSchemaExpired bool // cert is the certificate to use for validation. - // The caller is responsible for checking the expiry of the cert. cert ndn.Data + // certExpiryHandled indicates the expiry policy accepted the current Data/cert relation. + certExpiryHandled bool // certSigCov is the signature covered certificate wire. certSigCov enc.Wire // certRaw is the raw certificate bytes (if fetched). @@ -140,9 +146,25 @@ type TrustConfigValidateArgs struct { // depth is the maximum depth of the validation chain. depth int +} - // Use alternate verification flow. - UseSignatureTime optional.Optional[bool] +// runCertExpiryPolicy invokes the policy for a validation relation already known +// to involve an expired validity period. An asynchronous policy may race its +// result against a timeout, so only the first completion resumes validation. +func runCertExpiryPolicy( + policy ndn.CertExpiredCallback, + expiryArgs ndn.CertExpiredCallbackArgs, + resumeValidation func(error), +) { + if policy == nil { + policy = RejectExpiredCert // the default + } + var resumeOnce sync.Once + policy(expiryArgs, func(err error) { + resumeOnce.Do(func() { + resumeValidation(err) + }) + }) } // Validate validates a Data packet using a fetch API. @@ -179,14 +201,6 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { return } - // Bail if the data is a cert and is not fresh - if t, ok := args.Data.ContentType().Get(); ok && t == ndn.ContentTypeKey { - if !args.UseSignatureTime.GetOr(false) && CertIsExpired(args.Data) && !args.IgnoreValidity.GetOr(false) { - args.Callback(false, fmt.Errorf("certificate is expired: %s", args.Data.Name())) - return - } - } - // Get the key locator keyLocator := signature.KeyName() if len(keyLocator) == 0 { @@ -208,11 +222,29 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { return } - if !args.IgnoreValidity.GetOr(false) && CertIsExpired(args.cert) { - if args.UseSignatureTime.GetOr(false) && !ValidateSigTime(args.Data, args.cert) { - args.Callback(false, fmt.Errorf("data not signed during validity period: %s", args.cert.Name())) - return - } + // The same expired, intermidiary certificate would appear in callbacks two times. + // The first it appears as a signing certificate, asking if the data signing was valid. + // The second it appears as a data to be validated in the next depth, asking if the certificate issuance was valid. + // It is the responsibility of the policy to distinguish these two cases be aware of the cross schema usage. + certDataExpired := false + if t, ok := args.Data.ContentType().Get(); ok && t == ndn.ContentTypeKey { + certDataExpired = CertIsExpired(args.Data) + } + if !args.certExpiryHandled && + (args.crossSchemaExpired || certDataExpired || CertIsExpired(args.cert)) { + runCertExpiryPolicy(args.OnCertExpired, ndn.CertExpiredCallbackArgs{ + Data: args.Data, + Cert: args.cert, + }, func(err error) { + if err != nil { + args.Callback(false, err) + return + } + args.certExpiryHandled = true + args.depth++ // Resume the current validation depth. + tc.Validate(args) + }) + return } // Check schema if the key is allowed @@ -235,12 +267,10 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { args.Callback(valid, fmt.Errorf("cross schema: %w", err)) } }, - OverrideName: args.OverrideName, - IgnoreValidity: args.IgnoreValidity, - cert: args.cert, - depth: args.depth, - - UseSignatureTime: args.UseSignatureTime, + OverrideName: args.OverrideName, + OnCertExpired: args.OnCertExpired, + cert: args.cert, + depth: args.depth, }) return } else { @@ -277,8 +307,8 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { origCallback := args.Callback args.Callback = func(valid bool, err error) { if valid && err == nil { - // Cache is thread safe - tc.certCache.Put(args.cert) + // Cache the certificate and the wire needed to revalidate its chain. + tc.certCache.put(args.cert, args.certSigCov) // Keychain is not thread safe for inserts if len(args.certRaw) > 0 { @@ -301,11 +331,11 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { Data: args.cert, DataSigCov: args.certSigCov, - Fetch: args.Fetch, - Callback: args.Callback, - OverrideName: nil, - IgnoreValidity: args.IgnoreValidity, - origDataName: args.origDataName, + Fetch: args.Fetch, + Callback: args.Callback, + OverrideName: nil, + OnCertExpired: args.OnCertExpired, + origDataName: args.origDataName, cert: nil, certSigCov: nil, @@ -314,8 +344,6 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { crossSchemaIsValid: false, - UseSignatureTime: args.UseSignatureTime, - depth: args.depth, }) return @@ -333,13 +361,16 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { args.certSigCov = nil args.certRaw = nil args.certIsValid = false + args.certExpiryHandled = false args.crossSchemaIsValid = false - // Check the validated memcache for the certificate - if cachedCert, ok := tc.certCache.Get(keyLocator); ok { - // The cache always checks the expiry of the cert - args.cert = cachedCert - args.certIsValid = true + // A cache hit supplies evidence, not a reusable validation result. Only an + // exact trust anchor terminates the chain; every other chain is revalidated. + if cached, ok := tc.certCache.get(keyLocator); ok && + (tc.isTrustAnchor(cached.data.Name()) || len(cached.sigCovered) > 0) { + args.cert = cached.data + args.certSigCov = cached.sigCovered + args.certIsValid = tc.isTrustAnchor(cached.data.Name()) // Continue validation with cached cert tc.Validate(args) @@ -384,20 +415,14 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { return } - // Bail if the fetched cert is not fresh and not using signature time flow - if !args.UseSignatureTime.GetOr(false) && CertIsExpired(res.Data) && !args.IgnoreValidity.GetOr(false) { - args.Callback(false, fmt.Errorf("certificate is expired: %s", res.Data.Name())) - return - } - - // Fetched cert is fresh + // The certificate's expiry policy is checked when it is used below. log.Debug(tc, "Fetched certificate from network", "cert", res.Data.Name()) // Call again with the fetched cert args.cert = res.Data args.certSigCov = res.SigCovered args.certRaw = utils.If(!res.IsLocal, res.RawData, nil) // prevent double insert - args.certIsValid = false + args.certIsValid = tc.isTrustAnchor(res.Data.Name()) // Continue validation with fetched cert tc.Validate(args) @@ -419,22 +444,29 @@ func (tc *TrustConfig) validateCrossSchema(args TrustConfigValidateArgs) { return } - // Check validity period of the cross schema - if !args.IgnoreValidity.GetOr(false) { - if args.UseSignatureTime.GetOr(false) { - // Cross schema was valid at signature time - if CertIsExpired(crossData) && !ValidateSigTime(args.Data, crossData) { - args.Callback(false, fmt.Errorf("cross schema signature time invalid: %s", crossData.Name())) - return - } - } else { - if CertIsExpired(crossData) { - args.Callback(false, fmt.Errorf("cross schema is expired: %s", crossData.Name())) + // Check validity period of the cross schema. + if CertIsExpired(crossData) { + runCertExpiryPolicy(args.OnCertExpired, ndn.CertExpiredCallbackArgs{ + Data: args.Data, + Cert: crossData, + }, func(err error) { + if err != nil { + args.Callback(false, err) return } - } + tc.validateCrossSchemaData(args, crossData, crossDataSigCov, true) + }) + return } + tc.validateCrossSchemaData(args, crossData, crossDataSigCov, false) +} +func (tc *TrustConfig) validateCrossSchemaData( + args TrustConfigValidateArgs, + crossData ndn.Data, + crossDataSigCov enc.Wire, + crossSchemaExpired bool, +) { // Parse the cross schema content cross, err := trust_schema.ParseCrossSchemaContent(enc.NewWireView(crossData.Content()), false) if err != nil { @@ -458,18 +490,37 @@ func (tc *TrustConfig) validateCrossSchema(args TrustConfigValidateArgs) { Data: crossData, DataSigCov: crossDataSigCov, - Fetch: args.Fetch, - Callback: args.Callback, - OverrideName: dataName, // original data - IgnoreValidity: args.IgnoreValidity, + Fetch: args.Fetch, + Callback: args.Callback, + OverrideName: dataName, // original data + OnCertExpired: args.OnCertExpired, + crossSchemaExpired: crossSchemaExpired, depth: args.depth, - - UseSignatureTime: args.UseSignatureTime, }) } func (tc *TrustConfig) handleSelfSignedCert(args TrustConfigValidateArgs, keyLocator enc.Name) { + certDataExpired := false + if t, ok := args.Data.ContentType().Get(); ok && t == ndn.ContentTypeKey && CertIsExpired(args.Data) { + certDataExpired = true + } + if !args.certExpiryHandled && (args.crossSchemaExpired || certDataExpired) { + runCertExpiryPolicy(args.OnCertExpired, ndn.CertExpiredCallbackArgs{ + Data: args.Data, + Cert: args.Data, + }, func(err error) { + if err != nil { + args.Callback(false, err) + return + } + args.certExpiryHandled = true + args.depth++ // Resume the current validation depth. + tc.Validate(args) + }) + return + } + if len(args.DataSigCov) == 0 { args.Callback(false, fmt.Errorf("cert sig covered is nil: %s", args.Data.Name())) return @@ -548,11 +599,23 @@ func (tc *TrustConfig) isTrustedAnchorKey(keyLocator enc.Name) bool { return false } +func (tc *TrustConfig) isTrustAnchor(name enc.Name) bool { + tc.mutex.RLock() + defer tc.mutex.RUnlock() + for _, root := range tc.roots { + if root.Equal(name) { + return true + } + } + return false +} + type certListArgs struct { args TrustConfigValidateArgs anchorCert ndn.Data anchorRaw enc.Wire anchorKey enc.Name + listData ndn.Data visitedLists map[string]struct{} visitedCerts map[string]struct{} } @@ -622,6 +685,7 @@ func (tc *TrustConfig) processCertList(args certListArgs, listData ndn.Data, lis log.Warn(tc, "Failed to store CertList", "name", listData.Name(), "err", err) } } + args.listData = listData tc.tryListedCerts(args, names, 0) } @@ -634,6 +698,7 @@ func (tc *TrustConfig) tryListedCerts(args certListArgs, names []enc.Name, idx i name := names[idx] if !args.anchorKey.IsPrefix(name) { log.Debug(tc, "redirected cert name mismatch", "anchor", args.anchorKey, "redirect", name) + tc.tryListedCerts(args, names, idx+1) return } if _, ok := args.visitedCerts[name.TlvStr()]; ok { @@ -642,9 +707,9 @@ func (tc *TrustConfig) tryListedCerts(args certListArgs, names []enc.Name, idx i } args.visitedCerts[name.TlvStr()] = struct{}{} - if _, ok := tc.certCache.Get(name); ok { - tc.PromoteAnchor(args.anchorCert, args.anchorRaw) - args.args.Callback(true, nil) + if cached, ok := tc.certCache.get(name); ok && + (tc.isTrustAnchor(cached.data.Name()) || len(cached.sigCovered) > 0) { + tc.validateListedCert(args, names, idx, cached.data, cached.sigCovered, nil) return } @@ -667,37 +732,83 @@ func (tc *TrustConfig) tryListedCerts(args certListArgs, names []enc.Name, idx i return } - if t, ok := res.Data.ContentType().Get(); !ok || t != ndn.ContentTypeKey { - tc.tryListedCerts(args, names, idx+1) - return - } + tc.validateListedCert(args, names, idx, res.Data, res.SigCovered, res.RawData) + }) +} +func (tc *TrustConfig) validateListedCert( + args certListArgs, + names []enc.Name, + idx int, + cert ndn.Data, + certSigCov enc.Wire, + certRaw enc.Wire, +) { + next := func() { + tc.tryListedCerts(args, names, idx+1) + } + if t, ok := cert.ContentType().Get(); !ok || t != ndn.ContentTypeKey { + next() + return + } + if !bytes.Equal(cert.Content().Join(), args.anchorCert.Content().Join()) { + next() + return + } + if tc.isTrustAnchor(cert.Name()) { + tc.validateCertListSigner(args, cert, func() { + tc.PromoteAnchor(args.anchorCert, args.anchorRaw) + args.args.Callback(true, nil) + }, next) + return + } + + tc.validateCertListSigner(args, cert, func() { tc.Validate(TrustConfigValidateArgs{ - Data: res.Data, - DataSigCov: res.SigCovered, + Data: cert, + DataSigCov: certSigCov, Fetch: args.args.Fetch, UseDataNameFwHint: args.args.UseDataNameFwHint, Callback: func(valid bool, err error) { if valid && err == nil { - tc.certCache.Put(res.Data) - if len(res.RawData) > 0 { + tc.certCache.put(cert, certSigCov) + if len(certRaw) > 0 { tc.mutex.Lock() - _ = tc.keychain.InsertCert(res.RawData.Join()) + _ = tc.keychain.InsertCert(certRaw.Join()) tc.mutex.Unlock() } tc.PromoteAnchor(args.anchorCert, args.anchorRaw) args.args.Callback(true, nil) return } - tc.tryListedCerts(args, names, idx+1) + next() }, - IgnoreValidity: args.args.IgnoreValidity, - origDataName: args.args.origDataName, - depth: args.args.depth, - - UseSignatureTime: args.args.UseSignatureTime, + OnCertExpired: args.args.OnCertExpired, + origDataName: args.args.origDataName, + depth: args.args.depth, }) + }, next) +} + +// validateCertListSigner applies the expiry policy to the CertList and the +// listed certificate that authenticates its signing key. Their key content was +// checked for equality before this function is called. +func (tc *TrustConfig) validateCertListSigner(args certListArgs, cert ndn.Data, onAccept, onReject func()) { + if !CertIsExpired(cert) { + onAccept() + return + } + + runCertExpiryPolicy(args.args.OnCertExpired, ndn.CertExpiredCallbackArgs{ + Data: args.listData, + Cert: cert, + }, func(err error) { + if err != nil { + onReject() + return + } + onAccept() }) } diff --git a/std/security/trust_config_test.go b/std/security/trust_config_test.go index ed01ed74..7519ef76 100644 --- a/std/security/trust_config_test.go +++ b/std/security/trust_config_test.go @@ -89,10 +89,10 @@ var tcTestKeyChain ndn.KeyChain = nil var tcTestFetchCount int = 0 type ValidateSyncOptions struct { - name string - signer ndn.Signer - crossSchema enc.Wire - ignoreValidity bool + name string + signer ndn.Signer + crossSchema enc.Wire + onCertExpired ndn.CertExpiredCallback } // Helper to validate a packet synchronously @@ -114,13 +114,13 @@ func validateSync(opts ValidateSyncOptions) bool { ch <- valid close(ch) }, - IgnoreValidity: optional.Some(opts.ignoreValidity), + OnCertExpired: opts.onCertExpired, }) return <-ch } // Helper to validate certificates -func validateCerts(certData ndn.Data, certDataSigCov enc.Wire, ignoreValidity bool) bool { +func validateCerts(certData ndn.Data, certDataSigCov enc.Wire, onCertExpired ndn.CertExpiredCallback) bool { ch := make(chan bool) go tcTestTrustConfig.Validate(sec.TrustConfigValidateArgs{ Data: certData, @@ -131,7 +131,7 @@ func validateCerts(certData ndn.Data, certDataSigCov enc.Wire, ignoreValidity bo ch <- valid close(ch) }, - IgnoreValidity: optional.Some(ignoreValidity), + OnCertExpired: onCertExpired, }) return <-ch } @@ -342,10 +342,16 @@ func testTrustConfigIntra(t *testing.T, schema ndn.TrustSchema) { // Signing with correct keys tcTestFetchCount = 0 + expiryCallbackCalled := false require.True(t, validateSync(ValidateSyncOptions{ name: "/test/alice/data1", signer: aliceSigner, + onCertExpired: func(_ ndn.CertExpiredCallbackArgs, complete func(error)) { + expiryCallbackCalled = true + complete(fmt.Errorf("unexpected expiry callback")) + }, })) + require.False(t, expiryCallbackCalled) require.Equal(t, 0, tcTestFetchCount) // have all certificates require.True(t, validateSync(ValidateSyncOptions{ name: "/test/bob/data1", @@ -529,6 +535,38 @@ func testTrustConfigIntra(t *testing.T, schema ndn.TrustSchema) { crossSchema: abInvite, })) + expiredInvite, err := trust_schema.SignCrossSchema(trust_schema.SignCrossSchemaArgs{ + Name: sname("/test/alice/32=INVITE/test/bob/v=2"), + Signer: aliceSigner, + Content: trust_schema.CrossSchemaContent{ + SimpleSchemaRules: []*trust_schema.SimpleSchemaRule{{ + NamePrefix: sname("/test/alice/app/test/bob"), + KeyLocator: &spec.KeyLocator{Name: sname("/test/bob/KEY")}, + }}, + }, + NotBefore: time.Now().Add(-2 * time.Hour), + NotAfter: time.Now().Add(-time.Hour), + }) + require.NoError(t, err) + expiredInviteData, _, err := spec.Spec{}.ReadData(enc.NewWireView(expiredInvite)) + require.NoError(t, err) + + var crossSchemaExpiryArgs []ndn.CertExpiredCallbackArgs + require.True(t, validateSync(ValidateSyncOptions{ + name: "/test/alice/app/test/bob/expired-invite", + signer: bobSigner, + crossSchema: expiredInvite, + onCertExpired: func(args ndn.CertExpiredCallbackArgs, complete func(error)) { + crossSchemaExpiryArgs = append(crossSchemaExpiryArgs, args) + complete(nil) + }, + })) + require.Len(t, crossSchemaExpiryArgs, 2) + require.Equal(t, "/test/alice/app/test/bob/expired-invite", crossSchemaExpiryArgs[0].Data.Name().String()) + require.True(t, expiredInviteData.Name().Equal(crossSchemaExpiryArgs[0].Cert.Name())) + require.True(t, expiredInviteData.Name().Equal(crossSchemaExpiryArgs[1].Data.Name())) + require.True(t, aliceCertData.Name().Equal(crossSchemaExpiryArgs[1].Cert.Name())) + require.False(t, validateSync(ValidateSyncOptions{ name: "/test/alice/app/test/alice/data1", signer: bobSigner, @@ -790,18 +828,32 @@ func testTrustConfigIntra(t *testing.T, schema ndn.TrustSchema) { tcTestT.Log(eveSigner.KeyLocator().String()) eveCertWire, eveCertData, eveSigCov := signCert(rootSigner, tu.NoErr(signer.MarshalSecret(eveSigner)), expiredOpts) network[eveCertData.Name().String()] = eveCertWire - require.False(t, validateCerts(eveCertData, eveSigCov, false)) - require.True(t, validateCerts(eveCertData, eveSigCov, true)) + require.False(t, validateCerts(eveCertData, eveSigCov, nil)) + require.True(t, validateCerts(eveCertData, eveSigCov, sec.IgnoreExpiredCert)) + require.True(t, validateCerts(eveCertData, eveSigCov, sec.ValidateAtSignatureTime)) require.False(t, validateSync(ValidateSyncOptions{ - name: "/test/eve/data1", - signer: eveSigner, - ignoreValidity: false, + name: "/test/eve/data1", + signer: eveSigner, })) require.True(t, validateSync(ValidateSyncOptions{ - name: "/test/eve/data2", - signer: eveSigner, - ignoreValidity: true, + name: "/test/eve/data2", + signer: eveSigner, + onCertExpired: sec.IgnoreExpiredCert, })) + var callbackArgs []ndn.CertExpiredCallbackArgs + require.True(t, validateSync(ValidateSyncOptions{ + name: "/test/eve/data3", + signer: eveSigner, + onCertExpired: func(args ndn.CertExpiredCallbackArgs, complete func(error)) { + callbackArgs = append(callbackArgs, args) + go complete(nil) + }, + })) + require.Len(t, callbackArgs, 2) + require.Equal(t, "/test/eve/data3", callbackArgs[0].Data.Name().String()) + require.True(t, eveCertData.Name().Equal(callbackArgs[0].Cert.Name())) + require.True(t, eveCertData.Name().Equal(callbackArgs[1].Data.Name())) + require.True(t, rootCertData.Name().Equal(callbackArgs[1].Cert.Name())) } // This is intended as the ultimate inter-domain trust config test. @@ -902,7 +954,7 @@ func testTrustConfigInter(t *testing.T, schema ndn.TrustSchema) { NotBefore: nb, NotAfter: na, })) - userCertData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(userCertWire)) + userCertData, userCertSigCov, _ := spec.Spec{}.ReadData(enc.NewWireView(userCertWire)) payload := enc.Wire{[]byte{0x01}} dataWire := tu.NoErr(spec.Spec{}.MakeData(n("/app/user/alice/data"), &ndn.DataConfig{ @@ -960,7 +1012,12 @@ func testTrustConfigInter(t *testing.T, schema ndn.TrustSchema) { }, } - validateOnce := func(trust *sec.TrustConfig, data ndn.Data, sigCov enc.Wire) (bool, error) { + validateWithPolicy := func( + trust *sec.TrustConfig, + data ndn.Data, + sigCov enc.Wire, + onCertExpired ndn.CertExpiredCallback, + ) (bool, error) { tcTestFetchCount = 0 done := make(chan struct { v bool @@ -976,10 +1033,14 @@ func testTrustConfigInter(t *testing.T, schema ndn.TrustSchema) { err error }{v: valid, err: err} }, + OnCertExpired: onCertExpired, }) res := <-done return res.v, res.err } + validateOnce := func(trust *sec.TrustConfig, data ndn.Data, sigCov enc.Wire) (bool, error) { + return validateWithPolicy(trust, data, sigCov, nil) + } for _, st := range stages { t.Run(st.name, func(t *testing.T) { @@ -1020,6 +1081,283 @@ func testTrustConfigInter(t *testing.T, schema ndn.TrustSchema) { } }) } + + t.Run("cached intermediate revalidates expired parent", func(t *testing.T) { + clear(network) + + ownerExpiry := time.Now().Add(2 * time.Second) + expiringOwnerWire := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: rootSigner, + Data: ownerKeyData, + IssuerId: enc.NewGenericComponent("root-expiring"), + NotBefore: time.Now().Add(-time.Minute), + NotAfter: ownerExpiry, + })) + expiringOwnerData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(expiringOwnerWire)) + + freshPreAnchorWire := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: ownerSigner, + Data: anchorKeyData, + IssuerId: enc.NewGenericComponent("owner-fresh"), + NotBefore: time.Now().Add(-time.Minute), + NotAfter: time.Now().Add(time.Hour), + })) + freshPreAnchorData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(freshPreAnchorWire)) + + store := storage.NewMemoryStore() + tcTestKeyChain = keychain.NewKeyChainMem(store) + require.NoError(t, tcTestKeyChain.InsertCert(rootCertWire.Join())) + trust, err := sec.NewTrustConfig(tcTestKeyChain, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + + network[freshPreAnchorData.Name().String()] = freshPreAnchorWire + network[expiringOwnerData.Name().String()] = expiringOwnerWire + + valid, err := validateWithPolicy(trust, userCertData, userCertSigCov, nil) + require.True(t, valid) + require.NoError(t, err) + clear(network) + require.NoError(t, tcTestKeyChain.Store().Remove(freshPreAnchorData.Name())) + require.NoError(t, tcTestKeyChain.Store().Remove(expiringOwnerData.Name())) + + time.Sleep(time.Until(ownerExpiry) + 100*time.Millisecond) + + var expiryArgs []ndn.CertExpiredCallbackArgs + valid, err = validateWithPolicy(trust, userCertData, userCertSigCov, + func(args ndn.CertExpiredCallbackArgs, complete func(error)) { + expiryArgs = append(expiryArgs, args) + complete(fmt.Errorf("expired parent")) + }) + require.False(t, valid) + require.Error(t, err) + require.Len(t, expiryArgs, 1) + require.True(t, freshPreAnchorData.Name().Equal(expiryArgs[0].Data.Name())) + require.True(t, expiringOwnerData.Name().Equal(expiryArgs[0].Cert.Name())) + require.Equal(t, 0, tcTestFetchCount) + }) + + t.Run("certlist target must contain anchor key", func(t *testing.T) { + clear(network) + + maliciousAnchorSigner := tu.NoErr(signer.KeygenEd25519(anchorSigner.KeyName())) + maliciousAnchorKeyData := tu.NoErr(signer.MarshalSecretToData(maliciousAnchorSigner)) + maliciousAnchorWire := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: maliciousAnchorSigner, + Data: maliciousAnchorKeyData, + IssuerId: enc.NewGenericComponent("malicious-self"), + NotBefore: nb, + NotAfter: na, + })) + maliciousAnchorData, maliciousAnchorSigCov, _ := spec.Spec{}.ReadData(enc.NewWireView(maliciousAnchorWire)) + + maliciousListContent := tu.NoErr(sec.EncodeCertList([]enc.Name{preAnchorData.Name()})) + maliciousListName := listPrefix.Append(enc.NewVersionComponent(uint64(time.Now().UnixMicro()))) + maliciousListWire := tu.NoErr(spec.Spec{}.MakeData(maliciousListName, &ndn.DataConfig{ + Freshness: optional.Some(time.Minute), + }, maliciousListContent, maliciousAnchorSigner)) + + store := storage.NewMemoryStore() + tcTestKeyChain = keychain.NewKeyChainMem(store) + require.NoError(t, tcTestKeyChain.InsertCert(rootCertWire.Join())) + trust, err := sec.NewTrustConfig(tcTestKeyChain, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + + network[maliciousListName.String()] = maliciousListWire.Wire + network[preAnchorData.Name().String()] = preAnchorWire + network[ownerCertData.Name().String()] = ownerCertWire + + valid, err := validateWithPolicy(trust, maliciousAnchorData, maliciousAnchorSigCov, nil) + require.False(t, valid) + require.Error(t, err) + require.Contains(t, err.Error(), "no chain") + }) + + t.Run("rejected certlist candidate does not taint next candidate", func(t *testing.T) { + clear(network) + + expiredCandidateWire := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: ownerSigner, + Data: anchorKeyData, + IssuerId: enc.NewGenericComponent("expired-first"), + NotBefore: time.Now().Add(-2 * time.Hour), + NotAfter: time.Now().Add(-time.Hour), + })) + expiredCandidateData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(expiredCandidateWire)) + candidateListContent := tu.NoErr(sec.EncodeCertList([]enc.Name{ + expiredCandidateData.Name(), + preAnchorData.Name(), + })) + candidateListName := listPrefix.Append(enc.NewVersionComponent(uint64(time.Now().UnixMicro()))) + candidateListWire := tu.NoErr(spec.Spec{}.MakeData(candidateListName, &ndn.DataConfig{ + Freshness: optional.Some(time.Minute), + }, candidateListContent, anchorSigner)) + + store := storage.NewMemoryStore() + tcTestKeyChain = keychain.NewKeyChainMem(store) + require.NoError(t, tcTestKeyChain.InsertCert(rootCertWire.Join())) + trust, err := sec.NewTrustConfig(tcTestKeyChain, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + + network[candidateListName.String()] = candidateListWire.Wire + network[expiredCandidateData.Name().String()] = expiredCandidateWire + network[preAnchorData.Name().String()] = preAnchorWire + network[ownerCertData.Name().String()] = ownerCertWire + + policyCalls := 0 + valid, err := validateWithPolicy(trust, anchorCertData, anchorSigCov, + func(_ ndn.CertExpiredCallbackArgs, complete func(error)) { + policyCalls++ + complete(fmt.Errorf("reject first candidate")) + }) + require.True(t, valid) + require.NoError(t, err) + require.Equal(t, 1, policyCalls) + + clear(network) + require.NoError(t, tcTestKeyChain.Store().Remove(preAnchorData.Name())) + require.NoError(t, tcTestKeyChain.Store().Remove(ownerCertData.Name())) + valid, err = validateWithPolicy(trust, anchorCertData, anchorSigCov, nil) + require.True(t, valid) + require.NoError(t, err) + require.Equal(t, 0, tcTestFetchCount) + }) + + t.Run("cached expired certlist target", func(t *testing.T) { + clear(network) + + preAnchorExpiry := time.Now().Add(2 * time.Second) + expiredPreAnchorTemplate := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: ownerSigner, + Data: anchorKeyData, + IssuerId: enc.NewGenericComponent("expired-owner"), + NotBefore: now.Add(-time.Hour), + NotAfter: preAnchorExpiry, + })) + expiredPreAnchorTemplateData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(expiredPreAnchorTemplate)) + expiredPreAnchorEncoded := tu.NoErr(spec.Spec{}.MakeData( + expiredPreAnchorTemplateData.Name(), + &ndn.DataConfig{ + ContentType: optional.Some(ndn.ContentTypeKey), + Freshness: optional.Some(time.Hour), + SigNotBefore: optional.Some(now.Add(-time.Hour)), + SigNotAfter: optional.Some(preAnchorExpiry), + SigTime: optional.Some(time.Duration(now.Add(-30*time.Minute).UnixMilli()) * time.Millisecond), + }, + expiredPreAnchorTemplateData.Content(), + signer.AsContextSigner(ownerSigner), + )) + expiredPreAnchorWire := expiredPreAnchorEncoded.Wire + expiredPreAnchorData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(expiredPreAnchorWire)) + + expiredListContent := tu.NoErr(sec.EncodeCertList([]enc.Name{expiredPreAnchorData.Name()})) + expiredListName := listPrefix.Append(enc.NewVersionComponent(uint64(time.Now().UnixMicro()))) + expiredListWire := tu.NoErr(spec.Spec{}.MakeData(expiredListName, &ndn.DataConfig{ + Freshness: optional.Some(time.Minute), + }, expiredListContent, anchorSigner)) + expiredListData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(expiredListWire.Wire)) + + store := storage.NewMemoryStore() + tcTestKeyChain = keychain.NewKeyChainMem(store) + require.NoError(t, tcTestKeyChain.InsertCert(rootCertWire.Join())) + trust, err := sec.NewTrustConfig(tcTestKeyChain, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + + network[expiredPreAnchorData.Name().String()] = expiredPreAnchorWire + network[ownerCertData.Name().String()] = ownerCertWire + network[expiredListName.String()] = expiredListWire.Wire + + // Cache the target while it is fresh, then let it expire. + valid, err := validateWithPolicy(trust, userCertData, userCertSigCov, nil) + require.True(t, valid) + require.NoError(t, err) + time.Sleep(time.Until(preAnchorExpiry) + 100*time.Millisecond) + + var rejectedExpiryArgs []ndn.CertExpiredCallbackArgs + rejectExpired := func(args ndn.CertExpiredCallbackArgs, complete func(error)) { + rejectedExpiryArgs = append(rejectedExpiryArgs, args) + if len(rejectedExpiryArgs) == 1 { + complete(nil) + return + } + complete(fmt.Errorf("expired certlist target chain")) + } + valid, err = validateWithPolicy(trust, anchorCertData, anchorSigCov, rejectExpired) + require.False(t, valid) + require.Error(t, err) + require.Len(t, rejectedExpiryArgs, 2) + require.True(t, expiredListData.Name().Equal(rejectedExpiryArgs[0].Data.Name())) + require.True(t, expiredPreAnchorData.Name().Equal(rejectedExpiryArgs[0].Cert.Name())) + require.True(t, expiredPreAnchorData.Name().Equal(rejectedExpiryArgs[1].Data.Name())) + require.True(t, ownerCertData.Name().Equal(rejectedExpiryArgs[1].Cert.Name())) + require.Equal(t, 1, tcTestFetchCount) // CertList; target certificate is reloaded locally. + + var expiryArgs []ndn.CertExpiredCallbackArgs + validateAtSignatureTime := func(args ndn.CertExpiredCallbackArgs, complete func(error)) { + expiryArgs = append(expiryArgs, args) + sec.ValidateAtSignatureTime(args, complete) + } + valid, err = validateWithPolicy(trust, anchorCertData, anchorSigCov, validateAtSignatureTime) + require.False(t, valid) + require.Error(t, err) + require.Len(t, expiryArgs, 2) + require.True(t, expiredListData.Name().Equal(expiryArgs[0].Data.Name())) + require.True(t, expiredPreAnchorData.Name().Equal(expiryArgs[0].Cert.Name())) + require.True(t, expiredPreAnchorData.Name().Equal(expiryArgs[1].Data.Name())) + require.True(t, ownerCertData.Name().Equal(expiryArgs[1].Cert.Name())) + require.Equal(t, 0, tcTestFetchCount) // CertList is cached; target is reloaded locally. + + valid, err = validateWithPolicy(trust, anchorCertData, anchorSigCov, sec.IgnoreExpiredCert) + require.True(t, valid) + require.NoError(t, err) + require.Equal(t, 0, tcTestFetchCount) + + // Accepting the expired relationship establishes the anchor for later + // validations without replaying its historical certification path. + valid, err = validateWithPolicy(trust, anchorCertData, anchorSigCov, nil) + require.True(t, valid) + require.NoError(t, err) + }) + + t.Run("expired certlist target checks list signature time", func(t *testing.T) { + clear(network) + + expiredPreAnchorWire := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: ownerSigner, + Data: anchorKeyData, + IssuerId: enc.NewGenericComponent("expired-list-signer"), + NotBefore: time.Now().Add(-2 * time.Hour), + NotAfter: time.Now().Add(-time.Hour), + })) + expiredPreAnchorData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(expiredPreAnchorWire)) + certListContent := tu.NoErr(sec.EncodeCertList([]enc.Name{expiredPreAnchorData.Name()})) + certListName := listPrefix.Append(enc.NewVersionComponent(uint64(time.Now().UnixMicro()))) + certListWire := tu.NoErr(spec.Spec{}.MakeData(certListName, &ndn.DataConfig{ + Freshness: optional.Some(time.Minute), + }, certListContent, anchorSigner)) + certListData, _, _ := spec.Spec{}.ReadData(enc.NewWireView(certListWire.Wire)) + + store := storage.NewMemoryStore() + tcTestKeyChain = keychain.NewKeyChainMem(store) + require.NoError(t, tcTestKeyChain.InsertCert(rootCertWire.Join())) + trust, err := sec.NewTrustConfig(tcTestKeyChain, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + + network[expiredPreAnchorData.Name().String()] = expiredPreAnchorWire + network[ownerCertData.Name().String()] = ownerCertWire + network[certListData.Name().String()] = certListWire.Wire + + var expiryArgs []ndn.CertExpiredCallbackArgs + valid, err := validateWithPolicy(trust, anchorCertData, anchorSigCov, + func(args ndn.CertExpiredCallbackArgs, complete func(error)) { + expiryArgs = append(expiryArgs, args) + sec.ValidateAtSignatureTime(args, complete) + }) + require.False(t, valid) + require.Error(t, err) + require.Len(t, expiryArgs, 1) + require.True(t, certListData.Name().Equal(expiryArgs[0].Data.Name())) + require.True(t, expiredPreAnchorData.Name().Equal(expiryArgs[0].Cert.Name())) + }) } // (AI GENERATED DESCRIPTION): Initializes an in‑memory store and key chain, loads an LVS trust schema, and runs trust configuration tests. @@ -1267,7 +1605,7 @@ func TestSignatureTimeValidationFlows(t *testing.T) { }{v: valid, err: err} }, - UseSignatureTime: optional.Some(true), + OnCertExpired: sec.ValidateAtSignatureTime, }) res := <-done return res.v, res.err @@ -1341,7 +1679,7 @@ func TestSignatureTimeValidationFlows(t *testing.T) { dataSigCov: dataSigCovInvalidSigTimeCrossSchema, expectAnchor: true, expectData: false, - expectErrPart: "cross schema signature time invalid", + expectErrPart: "data not signed during validity period", }, // Validate flows after the expired root is replaced diff --git a/std/sync/snapshot_node_history.go b/std/sync/snapshot_node_history.go index bf9de2df..3b1ff732 100644 --- a/std/sync/snapshot_node_history.go +++ b/std/sync/snapshot_node_history.go @@ -8,7 +8,6 @@ import ( "github.com/named-data/ndnd/std/log" "github.com/named-data/ndnd/std/ndn" "github.com/named-data/ndnd/std/ndn/svs_ps" - "github.com/named-data/ndnd/std/types/optional" ) const SnapHistoryIndexFreshness = time.Millisecond * 10 @@ -44,10 +43,9 @@ type SnapshotNodeHistory struct { // In Repo mode, all snapshots are fetched automtically for persistence. IsRepo bool - // UseSignatureTime checks validity period using signature time - UseSignatureTime optional.Optional[bool] - // IgnoreValidity ignores validity period in the validation chain - IgnoreValidity optional.Optional[bool] + // OnCertExpired decides whether an expired certificate may be used. + // A nil callback rejects expired certificates. + OnCertExpired ndn.CertExpiredCallback // repoKnown is the known snapshot sequence number. repoKnown SvMap[uint64] @@ -164,9 +162,8 @@ func (s *SnapshotNodeHistory) idxName(node enc.Name, boot uint64) enc.Name { // fetchIndex fetches the latest index for a remote node. func (s *SnapshotNodeHistory) fetchIndex(node enc.Name, boot uint64, known uint64) { s.Client.ConsumeExt(ndn.ConsumeExtArgs{ - Name: s.idxName(node, boot), - UseSignatureTime: s.UseSignatureTime, - IgnoreValidity: s.IgnoreValidity, + Name: s.idxName(node, boot), + OnCertExpired: s.OnCertExpired, Callback: func(cstate ndn.ConsumeState) { go s.handleIndex(node, boot, known, cstate) }, @@ -213,10 +210,9 @@ func (s *SnapshotNodeHistory) handleIndex(node enc.Name, boot uint64, known uint snapName := s.snapName(node, boot).WithVersion(seqNo) s.Client.ConsumeExt(ndn.ConsumeExtArgs{ - Name: snapName, - UseSignatureTime: s.UseSignatureTime, - IgnoreValidity: s.IgnoreValidity, - Callback: func(cstate ndn.ConsumeState) { snapC <- cstate }, + Name: snapName, + OnCertExpired: s.OnCertExpired, + Callback: func(cstate ndn.ConsumeState) { snapC <- cstate }, }) scstate := <-snapC diff --git a/std/sync/snapshot_node_latest.go b/std/sync/snapshot_node_latest.go index 0a5c369a..85104e78 100644 --- a/std/sync/snapshot_node_latest.go +++ b/std/sync/snapshot_node_latest.go @@ -7,7 +7,6 @@ import ( enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/log" "github.com/named-data/ndnd/std/ndn" - "github.com/named-data/ndnd/std/types/optional" ) // SnapshotNodeLatest is a snapshot strategy that takes a snapshot of the @@ -33,10 +32,9 @@ type SnapshotNodeLatest struct { SnapMe func(enc.Name) (enc.Wire, error) // Threshold is the number of updates before a snapshot is taken. Threshold uint64 - // UseSignatureTime checks validity period using signature time - UseSignatureTime optional.Optional[bool] - // IgnoreValidity ignores validity period in the validation chain - IgnoreValidity optional.Optional[bool] + // OnCertExpired decides whether an expired certificate may be used. + // A nil callback rejects expired certificates. + OnCertExpired ndn.CertExpiredCallback // pss is the struct from the svs layer. pss snapPsState @@ -121,9 +119,8 @@ func (s *SnapshotNodeLatest) snapName(node enc.Name, boot uint64) enc.Name { func (s *SnapshotNodeLatest) fetchSnap(node enc.Name, boot uint64) { // Discover the latest snapshot s.Client.ConsumeExt(ndn.ConsumeExtArgs{ - Name: s.snapName(node, boot), - UseSignatureTime: s.UseSignatureTime, - IgnoreValidity: s.IgnoreValidity, + Name: s.snapName(node, boot), + OnCertExpired: s.OnCertExpired, Callback: func(cstate ndn.ConsumeState) { if cstate.Error() != nil { // Do not try too fast in case NFD returns NACK diff --git a/std/sync/svs.go b/std/sync/svs.go index 40eca7a2..412d9240 100644 --- a/std/sync/svs.go +++ b/std/sync/svs.go @@ -71,10 +71,9 @@ type SvSyncOpts struct { // Passive mode does not send sign Sync Interests Passive bool - // UseSignatureTime checks validity period using signature time - UseSignatureTime optional.Optional[bool] - // IgnoreValidity ignores validity period in the validation chain - IgnoreValidity optional.Optional[bool] + // OnCertExpired decides whether an expired certificate may be used. + // A nil callback rejects expired certificates. + OnCertExpired ndn.CertExpiredCallback } type SvSyncUpdate struct { @@ -534,10 +533,9 @@ func (s *SvSync) onSyncData(dataWire enc.Wire) { // Validate signature s.o.Client.ValidateExt(ndn.ValidateExtArgs{ - Data: data, - SigCovered: sigCov, - UseSignatureTime: s.o.UseSignatureTime, - IgnoreValidity: s.o.IgnoreValidity, + Data: data, + SigCovered: sigCov, + OnCertExpired: s.o.OnCertExpired, Callback: func(valid bool, err error) { if !valid || err != nil { log.Warn(s, "SvSync failed to validate signature", "name", data.Name(), "valid", valid, "err", err) diff --git a/std/sync/svs_alo.go b/std/sync/svs_alo.go index 1187b488..befd2fa9 100644 --- a/std/sync/svs_alo.go +++ b/std/sync/svs_alo.go @@ -140,12 +140,12 @@ func NewSvsALO(opts SvsAloOpts) (*SvsALO, error) { }, s.state) } - // Override IgnoreValidity from SVS (incorrect but practical) + // Override the certificate expiry callback from SVS (incorrect but practical). if latest, ok := s.opts.Snapshot.(*SnapshotNodeLatest); ok { - latest.IgnoreValidity = s.opts.Svs.IgnoreValidity + latest.OnCertExpired = s.opts.Svs.OnCertExpired } if history, ok := s.opts.Snapshot.(*SnapshotNodeHistory); ok { - history.IgnoreValidity = s.opts.Svs.IgnoreValidity + history.OnCertExpired = s.opts.Svs.OnCertExpired } return s, nil diff --git a/std/sync/svs_alo_data.go b/std/sync/svs_alo_data.go index 27543385..11dad8a4 100644 --- a/std/sync/svs_alo_data.go +++ b/std/sync/svs_alo_data.go @@ -117,9 +117,8 @@ func (s *SvsALO) consumeCheck(node enc.Name) { func (s *SvsALO) consumeObject(node enc.Name, boot uint64, seq uint64) { fetchName := s.objectName(node, boot, seq) s.client.ConsumeExt(ndn.ConsumeExtArgs{ - Name: fetchName, - UseSignatureTime: s.opts.Svs.UseSignatureTime, - IgnoreValidity: s.opts.Svs.IgnoreValidity, + Name: fetchName, + OnCertExpired: s.opts.Svs.OnCertExpired, Callback: func(status ndn.ConsumeState) { s.mutex.Lock() defer s.mutex.Unlock()