Skip to content
3 changes: 3 additions & 0 deletions std/ndn/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 8 additions & 1 deletion std/ndn/spec_2022/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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),
Expand Down
114 changes: 113 additions & 1 deletion std/security/certificate.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
package security

import (
"crypto/sha256"
"fmt"
"io"
"time"

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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
44 changes: 41 additions & 3 deletions std/security/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Comment thread
Harsh23Kashyap marked this conversation as resolved.
func TestSignCertInvalid(t *testing.T) {
tu.SetT(t)

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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"))
Expand Down
18 changes: 18 additions & 0 deletions std/security/revocation_tlv/definitions.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading
Loading