Repository: scionproto/scion
Pull request #4625 (2024-09-17 16:23:52)

scion-pki: allow referencing predecessor certificates in payload
When creating a TRC update, support referencing the predecessor certificates. This simplifies the TRC ceremony, as the already included certificates do not need to be redistributed in case they do not change.

jiceatscion: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/4625)
<!-- Reviewable:end -->


Patch:
diff --git a/scion-pki/conf/trc.go b/scion-pki/conf/trc.go
index dd059f255..647d234f1 100644
--- a/scion-pki/conf/trc.go
+++ b/scion-pki/conf/trc.go
@@ -17,6 +17,7 @@ package conf
 import (
 	"crypto/x509"
 	"path/filepath"
+	"strconv"
 	"strings"
 
 	"github.com/scionproto/scion/pkg/addr"
@@ -59,12 +60,29 @@ func LoadTRC(file string) (TRC, error) {
 }
 
 // Certificates returns the specified certificates.
-func (cfg *TRC) Certificates() ([]*x509.Certificate, error) {
+func (cfg *TRC) Certificates(pred *cppki.TRC) ([]*x509.Certificate, error) {
 	if len(cfg.CertificateFiles) == 0 {
 		return nil, serrors.New("no cert_files specified")
 	}
+
 	certs := make([]*x509.Certificate, 0, len(cfg.CertificateFiles))
 	for _, certFile := range cfg.CertificateFiles {
+
+		if raw, ok := strings.CutPrefix(certFile, "predecessor:"); ok {
+			if pred == nil {
+				return nil, serrors.New("predecessor certificate requested on base TRC")
+			}
+			idx, err := strconv.Atoi(raw)
+			if err != nil {
+				return nil, serrors.Wrap("parsing predecessor index", err, "input", raw)
+			}
+			if idx < 0 || idx >= len(pred.Certificates) {
+				return nil, serrors.New("predecessor index out of bounds", "index", idx)
+			}
+			certs = append(certs, pred.Certificates[idx])
+			continue
+		}
+
 		if !strings.HasPrefix(certFile, "/") {
 			certFile = filepath.Join(cfg.relPath, certFile)
 		}
diff --git a/scion-pki/trcs/payload.go b/scion-pki/trcs/payload.go
index 0fa2d7bc9..056bcec42 100644
--- a/scion-pki/trcs/payload.go
+++ b/scion-pki/trcs/payload.go
@@ -70,7 +70,7 @@ To inspect the created asn.1 file you can use the openssl tool::
 				return err
 			}
 			prepareCfg(&cfg, pred)
-			trc, err := CreatePayload(cfg)
+			trc, err := CreatePayload(cfg, pred)
 			if err != nil {
 				return serrors.Wrap("failed to marshal TRC", err)
 			}
diff --git a/scion-pki/trcs/toasn.go b/scion-pki/trcs/toasn.go
index d563f4ee5..3051e1451 100644
--- a/scion-pki/trcs/toasn.go
+++ b/scion-pki/trcs/toasn.go
@@ -23,8 +23,8 @@ import (
 
 // CreatePayload creates the ASN.1 payload for the TRC from the given
 // configuration.
-func CreatePayload(cfg conf.TRC) (*cppki.TRC, error) {
-	certs, err := cfg.Certificates()
+func CreatePayload(cfg conf.TRC, pred *cppki.TRC) (*cppki.TRC, error) {
+	certs, err := cfg.Certificates(pred)
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/scion-pki/conf/trc_test.go b/scion-pki/conf/trc_test.go
index 6af098ee0..1055239b0 100644
--- a/scion-pki/conf/trc_test.go
+++ b/scion-pki/conf/trc_test.go
@@ -114,11 +114,21 @@ func TestTRCCertificates(t *testing.T) {
 		prepareCfg   func(*conf.TRC)
 		errMsg       string
 		expectedCrts []*x509.Certificate
+		pred         *cppki.TRC
 	}{
 		"valid": {
 			prepareCfg:   func(_ *conf.TRC) {},
 			expectedCrts: []*x509.Certificate{rVoting, sVoting},
 		},
+		"load from predecessor": {
+			prepareCfg: func(cfg *conf.TRC) {
+				cfg.CertificateFiles[0] = "predecessor:4"
+			},
+			expectedCrts: []*x509.Certificate{rVoting, sVoting},
+			pred: &cppki.TRC{
+				Certificates: []*x509.Certificate{4: rVoting},
+			},
+		},
 		"file not found": {
 			prepareCfg: func(cfg *conf.TRC) { cfg.CertificateFiles = []string{"notfound"} },
 			errMsg:     "no such file or directory",
@@ -128,7 +138,7 @@ func TestTRCCertificates(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			cfg := createTRC(t)
 			tc.prepareCfg(cfg)
-			crts, err := cfg.Certificates()
+			crts, err := cfg.Certificates(tc.pred)
 			if tc.errMsg != "" {
 				assert.Error(t, err)
 				assert.Contains(t, err.Error(), tc.errMsg)
diff --git a/scion-pki/testcrypto/testcrypto.go b/scion-pki/testcrypto/testcrypto.go
index 0a819333f..a641ee813 100644
--- a/scion-pki/testcrypto/testcrypto.go
+++ b/scion-pki/testcrypto/testcrypto.go
@@ -321,7 +321,7 @@ func createTRCs(cfg config) error {
 			CertificateFiles:  certFiles[isd],
 		}
 		sort.Strings(trcConf.CertificateFiles)
-		trc, err := trcs.CreatePayload(trcConf)
+		trc, err := trcs.CreatePayload(trcConf, nil)
 		if err != nil {
 			return serrors.Wrap("creating TRC payload", err, "isd", isd)
 		}
diff --git a/scion-pki/trcs/toasn_test.go b/scion-pki/trcs/toasn_test.go
index c12be7159..b245d4844 100644
--- a/scion-pki/trcs/toasn_test.go
+++ b/scion-pki/trcs/toasn_test.go
@@ -27,7 +27,7 @@ import (
 func TestMarshalPayload(t *testing.T) {
 	cfg, err := conf.LoadTRC("testdata/admin/ISD1-B1-S1.toml")
 	require.NoError(t, err)
-	trc, err := CreatePayload(cfg)
+	trc, err := CreatePayload(cfg, nil)
 	require.NoError(t, err)
 	raw, err := trc.Encode()
 	require.NoError(t, err)