diff --git a/std/ndn/spec.go b/std/ndn/spec.go index 59e32a46..80eac647 100644 --- a/std/ndn/spec.go +++ b/std/ndn/spec.go @@ -97,6 +97,9 @@ type DataConfig struct { SigNotBefore optional.Optional[time.Time] SigNotAfter optional.Optional[time.Time] + // Signed Data signature timestamp (added with #186). + SigTime optional.Optional[time.Duration] + // Cross Schema attachment CrossSchema enc.Wire } diff --git a/std/ndn/spec_2022/spec.go b/std/ndn/spec_2022/spec.go index b245705f..a985a4fc 100644 --- a/std/ndn/spec_2022/spec.go +++ b/std/ndn/spec_2022/spec.go @@ -51,8 +51,11 @@ func (d *Data) SigNonce() []byte { return nil } -// (AI GENERATED DESCRIPTION): Returns the signature timestamp of the Data packet (currently unimplemented and returns nil). +// SigTime returns the signature timestamp of the Data packet, or nil if not set. func (d *Data) SigTime() *time.Time { + if d.SignatureInfo != nil && d.SignatureInfo.SignatureTime.IsSet() { + return utils.IdPtr(time.UnixMilli(d.SignatureInfo.SignatureTime.Unwrap().Milliseconds())) + } return nil } @@ -295,6 +298,10 @@ func (Spec) MakeData(name enc.Name, config *ndn.DataConfig, content enc.Wire, si data.SignatureInfo.KeyLocator = &KeyLocator{Name: key} } + if config.SigTime.IsSet() { + data.SignatureInfo.SignatureTime = config.SigTime + } + if config.SigNotBefore.IsSet() && config.SigNotAfter.IsSet() { data.SignatureInfo.ValidityPeriod = &ValidityPeriod{ NotBefore: config.SigNotBefore.Unwrap().UTC().Format(TimeFmt), diff --git a/std/security/certificate.go b/std/security/certificate.go index 99060019..1aa56830 100644 --- a/std/security/certificate.go +++ b/std/security/certificate.go @@ -1,6 +1,7 @@ package security import ( + "crypto/sha256" "fmt" "io" "time" @@ -8,10 +9,47 @@ import ( enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/ndn" spec "github.com/named-data/ndnd/std/ndn/spec_2022" + revocationtlv "github.com/named-data/ndnd/std/security/revocation_tlv" sig "github.com/named-data/ndnd/std/security/signer" "github.com/named-data/ndnd/std/types/optional" ) +// RevocationReason is the reason code for a revocation record, +// per RFC 5280 / UCLA-IRL/ndnrevoke. +type RevocationReason uint64 + +// Revocation reason codes from RFC 5280 / UCLA-IRL/ndnrevoke. +// RFC 5280 slots 7 (removeFromCRL) and 8 are intentionally unused. +const ( + RevocationReasonUnspecified RevocationReason = 0 + RevocationReasonKeyCompromise RevocationReason = 1 + RevocationReasonCACompromise RevocationReason = 2 + RevocationReasonAffiliationChanged RevocationReason = 3 + RevocationReasonSuperseded RevocationReason = 4 + RevocationReasonCessationOfOperation RevocationReason = 5 + RevocationReasonCertificateHold RevocationReason = 6 + RevocationReasonPrivilegeWithdrawn RevocationReason = 9 + RevocationReasonAACompromise RevocationReason = 10 +) + +const defaultRevocationFreshness = 8760 * time.Hour + +// RevokeCertArgs are the arguments to RevokeCert. +type RevokeCertArgs struct { + // Cert is the certificate being revoked. + Cert ndn.Data + // Signer signs the revocation record Data packet. May be nil for unsigned records. + Signer ndn.Signer + // Reason is the revocation reason code. + Reason RevocationReason + // Timestamp is the revocation timestamp. Defaults to now. + Timestamp optional.Optional[time.Time] + // NotBefore marks data produced before this timestamp as still valid. + NotBefore optional.Optional[time.Time] + // Freshness is the FreshnessPeriod of the record Data packet. + Freshness optional.Optional[time.Duration] +} + // SignCertArgs are the arguments to SignCert. type SignCertArgs struct { // Signer is the private key used to sign the certificate. @@ -28,6 +66,9 @@ type SignCertArgs struct { Description map[string]string // CrossSchema to attach to the certificate. CrossSchema enc.Wire + // SigTime overrides the signature timestamp applied to the certificate. + // When unset, the certificate is signed at the current time. + SigTime optional.Optional[time.Time] } // SignCert signs a new NDN certificate with the given signer. @@ -67,6 +108,10 @@ func SignCert(args SignCertArgs) (enc.Wire, error) { SigNotAfter: optional.Some(args.NotAfter), CrossSchema: args.CrossSchema, } + if args.SigTime.IsSet() { + t := args.SigTime.Unwrap() + cfg.SigTime = optional.Some(time.Duration(t.UnixMilli()) * time.Millisecond) + } signer := sig.AsContextSigner(args.Signer) cert, err := spec.Spec{}.MakeData(certName, cfg, enc.Wire{pk}, signer) @@ -97,7 +142,7 @@ func SelfSign(args SignCertArgs) (wire enc.Wire, err error) { return SignCert(args) } -// (AI GENERATED DESCRIPTION): Returns true if the certificate’s signature is nil or its validity period does not include the current time. +// CertIsExpired reports whether the certificate is outside its validity period. func CertIsExpired(cert ndn.Data) bool { if cert.Signature() == nil { return true @@ -115,6 +160,73 @@ func CertIsExpired(cert ndn.Data) bool { return false } +// RevokeCert builds a signed revocation record Data packet for a certificate. +func RevokeCert(args RevokeCertArgs) (enc.Wire, error) { + if args.Cert == nil { + return nil, fmt.Errorf("certificate is nil") + } + + recordName, ok := revocationRecordName(args.Cert) + if !ok { + return nil, fmt.Errorf("invalid certificate name for revocation: %s", args.Cert.Name()) + } + + timestamp := args.Timestamp.GetOr(time.Now()) + hash := sha256.Sum256(args.Cert.Content().Join()) + record := revocationtlv.RevocationRecord{ + Timestamp: uint64(timestamp.UnixMilli()), + Reason: uint64(args.Reason), + PublicKeyHash: hash[:], + } + if nb, ok := args.NotBefore.Get(); ok { + record.NotBefore = optional.Some(uint64(nb.UnixMilli())) + } + + // Create revocation record data + cfg := &ndn.DataConfig{ + ContentType: optional.Some(ndn.ContentTypeKey), + Freshness: optional.Some(args.Freshness.GetOr(defaultRevocationFreshness)), + } + + encoded, err := spec.Spec{}.MakeData(recordName, cfg, record.Encode(), args.Signer) + if err != nil { + return nil, err + } + + return encoded.Wire, nil +} + +func revocationRecordName(cert ndn.Data) (enc.Name, bool) { + if cert == nil { + return nil, false + } + + certName := stripImplicitDigest(cert.Name()) + identity, err := GetIdentityFromCertName(certName) + if err != nil || !certName.At(-1).IsVersion() { + return nil, false + } + + recordName := make(enc.Name, len(certName), len(certName)+1) + copy(recordName, certName) + recordName[len(identity)] = enc.NewGenericComponent("REVOKE") + return recordName.Append(certName.At(-2)), true +} + +func isRevocationRecordName(name enc.Name) bool { + name = stripImplicitDigest(name) + if len(name) < 6 || name.At(-1).Typ != enc.TypeGenericNameComponent || !name.At(-2).IsVersion() { + return false + } + + certName := make(enc.Name, len(name)-1) + copy(certName, name.Prefix(-1)) + certName[len(name)-5] = enc.NewGenericComponent("KEY") + + _, err := GetIdentityFromCertName(certName) + return err == nil +} + // 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/certificate_test.go b/std/security/certificate_test.go index f3419c3b..9002e244 100644 --- a/std/security/certificate_test.go +++ b/std/security/certificate_test.go @@ -2,7 +2,9 @@ package security_test import ( "encoding/base64" + "fmt" "testing" + "time" enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/ndn" @@ -13,7 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -// (AI GENERATED DESCRIPTION): Tests that the `SignCert` function correctly returns an error when invoked with a nil signer and nil data. func TestSignCertInvalid(t *testing.T) { tu.SetT(t) @@ -25,7 +26,6 @@ func TestSignCertInvalid(t *testing.T) { require.Error(t, err) } -// (AI GENERATED DESCRIPTION): Verifies that a key can self‑sign its own certificate, checking the certificate name, public‑key content, validity period, and Ed25519 signature correctness. func TestSignCertSelf(t *testing.T) { tu.SetT(t) @@ -73,7 +73,6 @@ func TestSignCertSelf(t *testing.T) { require.True(t, tu.NoErr(signer.ValidateData(cert, certSigCov, cert))) } -// (AI GENERATED DESCRIPTION): Verifies that Alice’s signer can correctly sign a root certificate, producing a certificate with the expected name, content, signature format, validity period, and that the signature verifies against Alice’s own certificate. func TestSignCertOther(t *testing.T) { tu.SetT(t) @@ -174,6 +173,45 @@ func TestSignCertWithSignerCertName(t *testing.T) { require.True(t, tu.NoErr(signer.ValidateData(newCert, newSigCov, aliceCertData))) } +func TestCertificateRevocationRecord(t *testing.T) { + tu.SetT(t) + + aliceKey, _ := base64.StdEncoding.DecodeString(KEY_ALICE) + aliceKeyData, _, _ := spec_2022.Spec{}.ReadData(enc.NewBufferView(aliceKey)) + aliceSigner := tu.NoErr(signer.UnmarshalSecret(aliceKeyData)) + + aliceCertWire := tu.NoErr(sec.SignCert(sec.SignCertArgs{ + Signer: aliceSigner, + Data: aliceKeyData, + IssuerId: revocationTestIssuer(t), + NotBefore: T1, + NotAfter: T2, + })) + aliceCert, _, err := spec_2022.Spec{}.ReadData(enc.NewWireView(aliceCertWire)) + require.NoError(t, err) + + _, err = sec.RevokeCert(sec.RevokeCertArgs{Cert: nil}) + require.Error(t, err) + + recordWire, err := sec.RevokeCert(sec.RevokeCertArgs{ + Cert: aliceCert, + Signer: aliceSigner, + }) + require.NoError(t, err) + recordData, _, err := spec_2022.Spec{}.ReadData(enc.NewWireView(recordWire)) + require.NoError(t, err) + require.Contains(t, recordData.Name().String(), "/REVOKE/") + contentType, ok := recordData.ContentType().Get() + require.True(t, ok) + require.Equal(t, ndn.ContentTypeKey, contentType) + require.NotEmpty(t, recordData.Content()) +} + +func revocationTestIssuer(t *testing.T) enc.Component { + t.Helper() + return enc.NewGenericComponent(fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano())) +} + func TestEncodeDecodeCertList(t *testing.T) { tu.SetT(t) n1 := tu.NoErr(enc.NameFromStr("/ndn/alice/KEY/aa/self/v=1")) diff --git a/std/security/revocation_tlv/definitions.go b/std/security/revocation_tlv/definitions.go new file mode 100644 index 00000000..66e87b17 --- /dev/null +++ b/std/security/revocation_tlv/definitions.go @@ -0,0 +1,18 @@ +//go:generate gondn_tlv_gen +package revocationtlv + +import "github.com/named-data/ndnd/std/types/optional" + +// RevocationRecord content TLVs per UCLA-IRL/ndnrevoke. +// +// +tlv-model:ordered +type RevocationRecord struct { + //+field:natural + Timestamp uint64 `tlv:"0xC9"` + //+field:natural + Reason uint64 `tlv:"0xCB"` + //+field:binary + PublicKeyHash []byte `tlv:"0xCA"` + //+field:natural:optional + NotBefore optional.Optional[uint64] `tlv:"0xCD"` +} diff --git a/std/security/revocation_tlv/zz_generated.go b/std/security/revocation_tlv/zz_generated.go new file mode 100644 index 00000000..c10de755 --- /dev/null +++ b/std/security/revocation_tlv/zz_generated.go @@ -0,0 +1,248 @@ +// Code generated by ndn tlv codegen DO NOT EDIT. +package revocationtlv + +import ( + "io" + + enc "github.com/named-data/ndnd/std/encoding" +) + +type RevocationRecordEncoder struct { + Length uint +} + +type RevocationRecordParsingContext struct { +} + +func (encoder *RevocationRecordEncoder) Init(value *RevocationRecord) { + + l := uint(0) + l += 1 + l += uint(1 + enc.Nat(value.Timestamp).EncodingLength()) + l += 1 + l += uint(1 + enc.Nat(value.Reason).EncodingLength()) + if value.PublicKeyHash != nil { + l += 1 + l += uint(enc.TLNum(len(value.PublicKeyHash)).EncodingLength()) + l += uint(len(value.PublicKeyHash)) + } + if optval, ok := value.NotBefore.Get(); ok { + l += 1 + l += uint(1 + enc.Nat(optval).EncodingLength()) + } + encoder.Length = l + +} + +func (context *RevocationRecordParsingContext) Init() { + +} + +func (encoder *RevocationRecordEncoder) EncodeInto(value *RevocationRecord, buf []byte) { + + pos := uint(0) + + buf[pos] = byte(201) + pos += 1 + + buf[pos] = byte(enc.Nat(value.Timestamp).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + buf[pos] = byte(203) + pos += 1 + + buf[pos] = byte(enc.Nat(value.Reason).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + if value.PublicKeyHash != nil { + buf[pos] = byte(202) + pos += 1 + pos += uint(enc.TLNum(len(value.PublicKeyHash)).EncodeInto(buf[pos:])) + copy(buf[pos:], value.PublicKeyHash) + pos += uint(len(value.PublicKeyHash)) + } + if optval, ok := value.NotBefore.Get(); ok { + buf[pos] = byte(205) + pos += 1 + + buf[pos] = byte(enc.Nat(optval).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + + } +} + +func (encoder *RevocationRecordEncoder) Encode(value *RevocationRecord) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *RevocationRecordParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*RevocationRecord, error) { + + var handled_Timestamp bool = false + var handled_Reason bool = false + var handled_PublicKeyHash bool = false + var handled_NotBefore bool = false + + progress := -1 + _ = progress + + value := &RevocationRecord{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + for handled := false; !handled && progress < 4; progress++ { + switch typ { + case 201: + if progress+1 == 0 { + handled = true + handled_Timestamp = true + value.Timestamp = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.Timestamp = uint64(value.Timestamp<<8) | uint64(x) + } + } + } + case 203: + if progress+1 == 1 { + handled = true + handled_Reason = true + value.Reason = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.Reason = uint64(value.Reason<<8) | uint64(x) + } + } + } + case 202: + if progress+1 == 2 { + handled = true + handled_PublicKeyHash = true + value.PublicKeyHash = make([]byte, l) + _, err = reader.ReadFull(value.PublicKeyHash) + } + case 205: + if progress+1 == 3 { + handled = true + handled_NotBefore = true + { + optval := uint64(0) + optval = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + optval = uint64(optval<<8) | uint64(x) + } + } + value.NotBefore.Set(optval) + } + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + switch progress { + case 0 - 1: + handled_Timestamp = true + err = enc.ErrSkipRequired{Name: "Timestamp", TypeNum: 201} + case 1 - 1: + handled_Reason = true + err = enc.ErrSkipRequired{Name: "Reason", TypeNum: 203} + case 2 - 1: + handled_PublicKeyHash = true + value.PublicKeyHash = nil + case 3 - 1: + handled_NotBefore = true + value.NotBefore.Unset() + } + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_Timestamp && err == nil { + err = enc.ErrSkipRequired{Name: "Timestamp", TypeNum: 201} + } + if !handled_Reason && err == nil { + err = enc.ErrSkipRequired{Name: "Reason", TypeNum: 203} + } + if !handled_PublicKeyHash && err == nil { + value.PublicKeyHash = nil + } + if !handled_NotBefore && err == nil { + value.NotBefore.Unset() + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *RevocationRecord) Encode() enc.Wire { + encoder := RevocationRecordEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *RevocationRecord) Bytes() []byte { + return value.Encode().Join() +} + +func ParseRevocationRecord(reader enc.WireView, ignoreCritical bool) (*RevocationRecord, error) { + context := RevocationRecordParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} diff --git a/std/security/trust_config.go b/std/security/trust_config.go index b8e4ba9a..dfaa22f1 100644 --- a/std/security/trust_config.go +++ b/std/security/trust_config.go @@ -8,6 +8,7 @@ import ( "github.com/named-data/ndnd/std/log" "github.com/named-data/ndnd/std/ndn" spec "github.com/named-data/ndnd/std/ndn/spec_2022" + revocationtlv "github.com/named-data/ndnd/std/security/revocation_tlv" "github.com/named-data/ndnd/std/security/signer" "github.com/named-data/ndnd/std/security/trust_schema" "github.com/named-data/ndnd/std/types/optional" @@ -81,7 +82,6 @@ func NewTrustConfig(keyChain ndn.KeyChain, schema ndn.TrustSchema, roots []enc.N }, nil } -// (AI GENERATED DESCRIPTION): Returns the constant string `"trust-config"` for a `TrustConfig` value, enabling string formatting via the `fmt.Stringer` interface. func (tc *TrustConfig) String() string { return "trust-config" } @@ -94,6 +94,26 @@ func (tc *TrustConfig) Suggest(name enc.Name) ndn.Signer { return tc.schema.Suggest(name, tc.keychain) } +// InsertRevoke stores a revocation record Data packet. +func (tc *TrustConfig) InsertRevoke(wire enc.Wire) error { + if len(wire) == 0 { + return fmt.Errorf("revocation record wire is empty") + } + + data, _, err := spec.Spec{}.ReadData(enc.NewWireView(wire)) + if err != nil { + return fmt.Errorf("failed to parse revocation record: %w", err) + } + if !isRevocationRecordName(data.Name()) { + return fmt.Errorf("not a revocation record name: %s", data.Name()) + } + + tc.mutex.Lock() + defer tc.mutex.Unlock() + // HACK: revocation records live in keychain.Store() until we have a dedicated store. + return tc.keychain.Store().Put(data.Name(), wire.Join()) +} + // SetSchema atomically replaces the trust schema. func (tc *TrustConfig) SetSchema(schema ndn.TrustSchema) { if schema == nil { @@ -182,6 +202,9 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { args.Callback(false, fmt.Errorf("certificate is expired: %s", args.Data.Name())) return } + if tc.rejectIfRevokedCert(args.Data, args.Callback) { + return + } } // Get the key locator @@ -193,6 +216,10 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { // If a certificate is provided, go directly to validation if args.cert != nil { + if tc.rejectIfRevokedCert(args.cert, args.Callback) { + return + } + certName := args.cert.Name() dataName := args.Data.Name() if len(args.OverrideName) > 0 { @@ -323,6 +350,10 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { // Check the validated memcache for the certificate if cachedCert, ok := tc.certCache.Get(keyLocator); ok { + if tc.rejectIfRevokedCert(cachedCert, args.Callback) { + return + } + // The cache always checks the expiry of the cert args.cert = cachedCert args.certIsValid = true @@ -338,7 +369,8 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { fwHint = []enc.Name{args.origDataName} } - // Cert not found, attempt to fetch from network + // Cert not found, attempt to fetch from network. + // TODO(revocation): fetch REVOKE record by Interest when not in keychain.Store(). fetchCfg := &ndn.InterestConfig{ CanBePrefix: true, MustBeFresh: true, @@ -379,6 +411,10 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { // Fetched cert is fresh log.Debug(tc, "Fetched certificate from network", "cert", res.Data.Name()) + if tc.rejectIfRevokedCert(res.Data, args.Callback) { + return + } + // Call again with the fetched cert args.cert = res.Data args.certSigCov = res.SigCovered @@ -391,7 +427,6 @@ func (tc *TrustConfig) Validate(args TrustConfigValidateArgs) { args.Fetch(keyLocator, fetchCfg, cb) } -// (AI GENERATED DESCRIPTION): Validates the cross‑schema signed Data packet by parsing its embedded schema, checking its validity period, ensuring it authorizes the original certificate, and recursively validating the cross‑schema’s signature against the trust configuration. func (tc *TrustConfig) validateCrossSchema(args TrustConfigValidateArgs) { crossWire := args.Data.CrossSchema() if crossWire == nil { @@ -507,6 +542,57 @@ func (tc *TrustConfig) PromoteAnchor(cert ndn.Data, raw enc.Wire) { tc.roots = append(tc.roots, name) } +func (tc *TrustConfig) rejectIfRevokedCert(cert ndn.Data, callback func(bool, error)) bool { + if cert == nil { + return false + } + + recordName, ok := revocationRecordName(cert) + if !ok { + return false + } + + wire, _ := tc.keychain.Store().Get(recordName, false) + if len(wire) == 0 { + return false + } + // A cert signed before the record's notBefore was issued before the + // revocation took effect, so we accept it as still valid. + if preRevocationCert(cert, enc.Wire{wire}) { + return false + } + callback(false, fmt.Errorf("certificate is revoked: %s", cert.Name())) + return true +} + +// preRevocationCert returns true if the cert was signed before the +// installed record's NotBefore timestamp, in which case the cert predates +// the revocation and is still valid. +func preRevocationCert(cert ndn.Data, recordWire enc.Wire) bool { + sig := cert.Signature() + if sig == nil { + return false + } + sigTime := sig.SigTime() + if sigTime == nil { + return false + } + + data, _, err := spec.Spec{}.ReadData(enc.NewWireView(recordWire)) + if err != nil { + return false + } + record, err := revocationtlv.ParseRevocationRecord(enc.NewWireView(data.Content()), false) + if err != nil || record == nil { + return false + } + nb, ok := record.NotBefore.Get() + if !ok { + return false + } + return sigTime.UnixMilli() < int64(nb) +} + func (tc *TrustConfig) isTrustedAnchorKey(keyLocator enc.Name) bool { tc.mutex.RLock() defer tc.mutex.RUnlock() diff --git a/std/security/trust_config_test.go b/std/security/trust_config_test.go index 8ca26328..25044542 100644 --- a/std/security/trust_config_test.go +++ b/std/security/trust_config_test.go @@ -14,6 +14,7 @@ import ( "github.com/named-data/ndnd/std/object/storage" sec "github.com/named-data/ndnd/std/security" "github.com/named-data/ndnd/std/security/keychain" + revocationtlv "github.com/named-data/ndnd/std/security/revocation_tlv" "github.com/named-data/ndnd/std/security/signer" "github.com/named-data/ndnd/std/security/trust_schema" "github.com/named-data/ndnd/std/types/optional" @@ -67,7 +68,6 @@ type SignCertOptions struct { NotAfter time.Time } -// (AI GENERATED DESCRIPTION): Creates a signed certificate for the supplied Data packet using the provided signer, and returns the certificate wire, its content, and the signature‑covered portion. func signCert(signer ndn.Signer, wire enc.Wire, opts SignCertOptions) (enc.Wire, ndn.Data, enc.Wire) { data, _, _ := spec.Spec{}.ReadData(enc.NewWireView(wire)) cert, _ := sec.SignCert(sec.SignCertArgs{ @@ -495,8 +495,6 @@ func testTrustConfigIntra(t *testing.T, schema ndn.TrustSchema) { })) require.Equal(t, 4, tcTestFetchCount) // (same as root 1, except no mallory root fetch) - // ======================================================================== - // Test with cross schema validation // Alice signs a cross schema for bob to allow bob to publish in alice's namespace abInvite, err := trust_schema.SignCrossSchema(trust_schema.SignCrossSchemaArgs{ @@ -1022,7 +1020,6 @@ func testTrustConfigInter(t *testing.T, schema ndn.TrustSchema) { } } -// (AI GENERATED DESCRIPTION): Initializes an in‑memory store and key chain, loads an LVS trust schema, and runs trust configuration tests. func TestTrustConfigLvsIntra(t *testing.T) { tu.SetT(t) @@ -1044,3 +1041,166 @@ func TestTrustConfigLvsInter(t *testing.T) { testTrustConfigInter(t, schemaInter) } + +func makeRevocationRecordWire(t *testing.T, cert ndn.Data, signer ndn.Signer) enc.Wire { + t.Helper() + + wire, err := sec.RevokeCert(sec.RevokeCertArgs{ + Cert: cert, + Signer: signer, + }) + require.NoError(t, err) + return wire +} + +func TestTrustConfigRevocation(t *testing.T) { + tu.SetT(t) + + clear(tcTestNetwork) + tcTestT = t + network := tcTestNetwork + store := storage.NewMemoryStore() + keychain := keychain.NewKeyChainMem(store) + tcTestKeyChain = keychain + + opts := SignCertOptions{ + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + } + + rootSigner, _ := signer.KeygenEd25519(sec.MakeKeyName(sname("/test"))) + rootCertWire, rootCertData, _ := signCert(rootSigner, tu.NoErr(signer.MarshalSecret(rootSigner)), opts) + network[rootCertData.Name().String()] = rootCertWire + require.NoError(t, keychain.InsertCert(rootCertWire.Join())) + + aliceSigner, _ := signer.KeygenEd25519(sec.MakeKeyName(sname("/test/alice"))) + aliceCertWire, aliceCertData, _ := signCert(rootSigner, tu.NoErr(signer.MarshalSecret(aliceSigner)), opts) + network[aliceCertData.Name().String()] = aliceCertWire + require.NoError(t, keychain.InsertCert(aliceCertWire.Join())) + require.NoError(t, keychain.InsertKey(aliceSigner)) + + bobSigner, _ := signer.KeygenEd25519(sec.MakeKeyName(sname("/test/bob"))) + bobCertWire, bobCertData, _ := signCert(rootSigner, tu.NoErr(signer.MarshalSecret(bobSigner)), opts) + network[bobCertData.Name().String()] = bobCertWire + + schema, err := trust_schema.NewLvsSchema(TRUST_CONFIG_INTRA_LVS) + require.NoError(t, err) + + trust, err := sec.NewTrustConfig(keychain, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + tcTestTrustConfig = trust + + require.Error(t, trust.InsertRevoke(nil)) + require.Error(t, trust.InsertRevoke(aliceCertWire)) + + recordWire := makeRevocationRecordWire(t, aliceCertData, rootSigner) + require.NoError(t, trust.InsertRevoke(recordWire)) + + recordData, _, err := spec.Spec{}.ReadData(enc.NewWireView(recordWire)) + require.NoError(t, err) + if buf, _ := keychain.Store().Get(recordData.Name(), false); buf == nil { + t.Fatal("revocation record not in store") + } + + require.False(t, validateSync(ValidateSyncOptions{ + name: "/test/alice/data1", + signer: aliceSigner, + })) + + // Cached cert path. + tcTestFetchCount = 0 + require.True(t, validateSync(ValidateSyncOptions{ + name: "/test/bob/data1", + signer: bobSigner, + })) + require.Equal(t, 1, tcTestFetchCount) + + bobRecordWire := makeRevocationRecordWire(t, bobCertData, rootSigner) + require.NoError(t, trust.InsertRevoke(bobRecordWire)) + require.False(t, validateSync(ValidateSyncOptions{ + name: "/test/bob/data2", + signer: bobSigner, + })) + require.Equal(t, 1, tcTestFetchCount) +} + +func TestTrustConfigRevocationNotBefore(t *testing.T) { + tu.SetT(t) + + clear(tcTestNetwork) + tcTestT = t + store := storage.NewMemoryStore() + keychainLocal := keychain.NewKeyChainMem(store) + tcTestKeyChain = keychainLocal + + opts := SignCertOptions{ + NotBefore: time.Now(), + NotAfter: time.Now().Add(2 * time.Hour), + } + + rootSigner, _ := signer.KeygenEd25519(sec.MakeKeyName(sname("/test"))) + rootCertWire, rootCertData, _ := signCert(rootSigner, tu.NoErr(signer.MarshalSecret(rootSigner)), opts) + require.NoError(t, keychainLocal.InsertCert(rootCertWire.Join())) + + daveSigner, _ := signer.KeygenEd25519(sec.MakeKeyName(sname("/test/dave"))) + daveSec, _ := tu.NoErr(signer.MarshalSecret(daveSigner)), error(nil) + daveCertWire, daveCertData, _ := signCertWithSigTime(rootSigner, daveSec, opts, + time.Now().Add(-2*time.Hour)) + require.NoError(t, keychainLocal.InsertCert(daveCertWire.Join())) + require.NoError(t, keychainLocal.InsertKey(daveSigner)) + + schema, err := trust_schema.NewLvsSchema(TRUST_CONFIG_INTRA_LVS) + require.NoError(t, err) + + trust, err := sec.NewTrustConfig(keychainLocal, schema, []enc.Name{rootCertData.Name()}) + require.NoError(t, err) + tcTestTrustConfig = trust + + // Issue a revocation record with NotBefore set to the future. A cert whose + // signature timestamp is older than NotBefore must still validate. + recordWire := tu.NoErr(sec.RevokeCert(sec.RevokeCertArgs{ + Cert: daveCertData, + Signer: rootSigner, + Reason: sec.RevocationReasonKeyCompromise, + NotBefore: optional.Some(time.Now().Add(time.Hour)), + })) + require.NoError(t, trust.InsertRevoke(recordWire)) + + // Sanity-check: record wire has the chosen reason. + recordData, _, err := spec.Spec{}.ReadData(enc.NewWireView(recordWire)) + require.NoError(t, err) + record, err := revocationtlv.ParseRevocationRecord(enc.NewWireView(recordData.Content()), false) + require.NoError(t, err) + require.Equal(t, uint64(1), record.Reason) + + // Validation passes because the cert's signature timestamp pre-dates + // the record's NotBefore. + network := tcTestNetwork + network[daveCertData.Name().String()] = daveCertWire + require.True(t, validateSync(ValidateSyncOptions{ + name: "/test/dave/data1", + signer: daveSigner, + })) +} + +// signCertWithSigTime mirrors signCert but pins the new cert's signature +// timestamp via sec.SignCert's SigTime option. +func signCertWithSigTime(s ndn.Signer, wire enc.Wire, opts SignCertOptions, sigTime time.Time) (enc.Wire, ndn.Data, enc.Wire) { + data, _, _ := spec.Spec{}.ReadData(enc.NewWireView(wire)) + cert, err := sec.SignCert(sec.SignCertArgs{ + Signer: s, + Data: data, + IssuerId: enc.NewGenericComponent("ndn"), + NotBefore: opts.NotBefore, + NotAfter: opts.NotAfter, + SigTime: optional.Some(sigTime), + }) + if err != nil { + panic(err) + } + certData, sigCov, err := spec.Spec{}.ReadData(enc.NewWireView(cert)) + if err != nil { + panic(err) + } + return cert, certData, sigCov +}