Repository: scionproto/scion
Pull request #3798 (2020-06-22 14:11:52)

spki: make cert renewal easier to use
Improve the usability of the cert renewal tool.

No longer require the `out` and `template` flags.
No longer require the positional argument that indicates the CA to send
the certificate chain request to.

If the `template` flag is not set, the subject of the CSR is filled with
with the subject of the transport certificate chain.

If the `out` flag is not set, the resulting chain is written to the
directory of the transport certificate chain as `ISDx-ASy.s.pem`,
where `x` is the ISD, `y` is the AS number, and `s` is the hex encoded
serial number of the AS certificate in the certificate chain.

If the positional argument is not set, the CSR is sent to the subject
of the CA certificate in the transport certificate chain.

In case verification fails against the TRC, the chain is now still
written to disk with the suffix `.unverified`. This ensures that the
CS will not automatically pick up the chain. However, operators can
still inspect the resulting chain, and check if the verification error
is the result of using the wrong TRC.

Improve the help message to describe the behavior of the tool
and to describe the expected input for the template file.

<!-- 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/3798)
<!-- Reviewable:end -->



Patch:
diff --git a/go/scion-pki/certs/BUILD.bazel b/go/scion-pki/certs/BUILD.bazel
index a6299489c..51036b673 100644
--- a/go/scion-pki/certs/BUILD.bazel
+++ b/go/scion-pki/certs/BUILD.bazel
@@ -44,7 +44,6 @@ go_test(
         "//go/lib/sock/reliable:go_default_library",
         "//go/lib/xtest:go_default_library",
         "//go/pkg/trust:go_default_library",
-        "//go/proto:go_default_library",
         "@com_github_golang_mock//gomock:go_default_library",
         "@com_github_stretchr_testify//assert:go_default_library",
         "@com_github_stretchr_testify//require:go_default_library",
diff --git a/go/scion-pki/certs/renew.go b/go/scion-pki/certs/renew.go
index df39eb98e..28c6f7e03 100644
--- a/go/scion-pki/certs/renew.go
+++ b/go/scion-pki/certs/renew.go
@@ -28,7 +28,7 @@ import (
 	"io/ioutil"
 	"net"
 	"os"
-	"strings"
+	"path/filepath"
 	"time"
 
 	"github.com/spf13/cobra"
@@ -50,16 +50,16 @@ import (
 )
 
 type subjectVars struct {
-	CommonName         string `json:"common_name,omitempty"`
-	Country            string `json:"country,omitempty"`
-	ISDAS              string `json:"isd_as,omitempty"`
-	Locality           string `json:"locality,omitempty"`
-	Organization       string `json:"organization,omitempty"`
-	OrganizationalUnit string `json:"organizational_unit,omitempty"`
-	PostalCode         string `json:"postal_code,omitempty"`
-	Province           string `json:"province,omitempty"`
-	SerialNumber       string `json:"serial_number,omitempty"`
-	StreetAddress      string `json:"street_address,omitempty"`
+	CommonName         string  `json:"common_name,omitempty"`
+	Country            string  `json:"country,omitempty"`
+	ISDAS              addr.IA `json:"isd_as,omitempty"`
+	Locality           string  `json:"locality,omitempty"`
+	Organization       string  `json:"organization,omitempty"`
+	OrganizationalUnit string  `json:"organizational_unit,omitempty"`
+	PostalCode         string  `json:"postal_code,omitempty"`
+	Province           string  `json:"province,omitempty"`
+	SerialNumber       string  `json:"serial_number,omitempty"`
+	StreetAddress      string  `json:"street_address,omitempty"`
 }
 
 func newRenewCmd() *cobra.Command {
@@ -80,41 +80,109 @@ func newRenewCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "renew",
 		Short: "Renew AS certificate",
-		Args:  cobra.MinimumNArgs(1),
-		Example: strings.Join([]string{
-			"scion-pki certs renew",
-			"--template vars.json",
-			"--key /path/to/cp-as.key",
-			"--transportcert /path/to/ISD1-ASff00_0_112.pem",
-			"--transportkey /path/tocp-as.key",
-			"--trc /path/to/ISD1-B1-S1.trc",
-			"--timeout 5s",
-			"--out ISD1-ASff00_0_110.new.pem",
-			"1-ff00:0:110"}, " \\\n\t"),
+		Args:  cobra.MaximumNArgs(1),
+		Example: `  scion-pki certs renew
+	--key cp-as.key \
+	--transportcert ISD1-ASff00_0_112.pem \
+	--transportkey cp-as.key \
+	--trc ISD1-B1-S1.trc
+
+  scion-pki certs renew
+	--key fresh.key \
+	--transportcert ISD1-ASff00_0_112.pem \
+	--transportkey cp-as.key \
+	--trc ISD1-B1-S1.trc \
+	--template csr.json \
+	  1-ff00:0:110
+		`,
+		Long: `'renew' sends a certificate chain renewal request to the CA control service.
+
+The transport certificate chain and key are used to sign the renewal requests.
+In order for the CA to be able to verify the request, the chain must already
+be known to the CA. Either through an out-of-bound bootstrapping mechanism where
+the CA preloads it, or from a previous certificate chain renewal.
+
+The TRC is used to validate and verify the renewed certificate chain. Ensure
+that it contains the root certificate that the CA is using.
+
+The renewed certificate chain is written to the file system, if it is verifiable
+with the supplied TRC. In case the out flag is not specified, the chain is
+written to 'ISDx-ASy.s.pem' in the same directory as the transport certificate
+chain, where x is the ISD number, y is the AS number, and s is the hex encoded
+serial number of the AS certificate in the renewed certificate chain. If the
+chain verification against the TRC fails, the renewed certificate chain is
+written to the out file with the suffix '.unverified' and the command fails.
+
+The positional argument is the ISD-AS of the CA where the renewal request is
+sent to. If it is not set, the ISD-AS is extracted from the transport
+certificate chain.
+
+Unless a template is specified, the subject of the transport certificate chain
+is used as the subject for the renewal request.
+
+The template is expressed in JSON. A valid example:
+
+  {
+    "common_name": "1-ff00:0:110 AS certificate",
+    "country": "CH",
+    "isd_as": "1-ff00:0:110"
+  }
+
+All configurable fields with their type are defined by the following JSON
+schema. For more information on JSON schemas, see https://json-schema.org/.
+
+  {
+    "type": "object",
+    "properties": {
+      "isd_as":              { "type": "string" },
+      "common_name":         { "type": "string" },
+      "country":             { "type": "string" },
+      "locality":            { "type": "string" },
+      "organization":        { "type": "string" },
+      "organizational_unit": { "type": "string" },
+      "postal_code":         { "type": "string" },
+      "province":            { "type": "string" },
+      "serial_number":       { "type": "string" },
+      "street_address":      { "type": "string" },
+    },
+    "required": ["isd_as"]
+  }
+		`,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			remoteIA, err := addr.IAFromString(args[0])
-			if err != nil {
-				return err
+			var ca addr.IA
+			if len(args) != 0 {
+				var err error
+				if ca, err = addr.IAFromString(args[0]); err != nil {
+					return err
+				}
 			}
 			cmd.SilenceUsage = true
+
 			log.Setup(log.Config{Console: log.ConsoleConfig{Level: "crit"}})
 
 			trc, err := loadTRC(flags.trcFilePath)
 			if err != nil {
 				return err
 			}
+			chain, transportCA, err := loadChain(trc, flags.transportCertFile)
+			if err != nil {
+				return err
+			}
+			if ca.IsZero() {
+				ca = transportCA
+				fmt.Println("Extracted remote from transport certificate chain: ", ca)
+			}
 
 			// Step 1. create CSR.
-			vars, err := readVars(flags.templateFile)
+			tmpl, err := csrTemplate(chain, flags.templateFile)
 			if err != nil {
 				return err
 			}
-
 			key, err := readECKey(flags.keyFile)
 			if err != nil {
 				return err
 			}
-			csr, err := buildCSR(vars, key)
+			csr, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
 			if err != nil {
 				return err
 			}
@@ -133,7 +201,7 @@ func newRenewCmd() *cobra.Command {
 			}
 
 			remote := &snet.UDPAddr{
-				IA: remoteIA,
+				IA: ca,
 			}
 			disp := reliable.NewDispatcher(flags.dispatcherPath)
 			msgr, err := buildMsgr(ctx, disp, sds, local, remote)
@@ -142,30 +210,42 @@ func newRenewCmd() *cobra.Command {
 			}
 
 			// Step 3. renewal scion call.
-			chain, err := runRenew(ctx, csr, local.IA, remote.IA, trc,
-				flags.transportCertFile, flags.transportKeyFile, msgr)
+			signer, err := createSigner(local.IA, trc, chain, flags.transportKeyFile)
+			if err != nil {
+				return err
+			}
+			renewed, err := renew(ctx, csr, remote.IA, signer, msgr)
 			if err != nil {
 				return err
 			}
 
+			out := flags.outFile
+			if out == "" {
+				out = outFileFromSubject(renewed, filepath.Dir(flags.transportCertFile))
+			}
+
 			// Step 4. verify with trc.
-			if err := cppki.VerifyChain(chain, cppki.VerifyOptions{TRC: &trc.TRC}); err != nil {
+			if err := cppki.VerifyChain(renewed, cppki.VerifyOptions{TRC: &trc.TRC}); err != nil {
+				out += ".unverified"
+				fmt.Println("Verification failed, writing chain: ", out)
+				if err := writeChain(renewed, out); err != nil {
+					fmt.Println("Failed to write unverified chain: ", err)
+				}
 				return serrors.WrapStr("verification failed", err)
 			}
 
 			// Step 5. write to disk.
-			if err := writeChain(chain, flags.outFile); err != nil {
+			if err := writeChain(renewed, out); err != nil {
 				return err
 			}
 
-			fmt.Printf("Successfully wrote new chain at %s\n", flags.outFile)
+			fmt.Printf("Successfully wrote new chain at %s\n", out)
 			return nil
 		},
 	}
 
 	cmd.Flags().StringVar(&flags.templateFile, "template", "",
-		"File with data for the CSR in json format (required)")
-	cmd.MarkFlagRequired("template")
+		"File with data for the CSR in json format")
 	cmd.Flags().StringVar(&flags.keyFile, "key", "",
 		"Private key file to sign the CSR (required)")
 	cmd.MarkFlagRequired("key")
@@ -186,29 +266,35 @@ func newRenewCmd() *cobra.Command {
 	cmd.Flags().IPVarP(&flags.listen, "local", "l", nil,
 		"Optional local IP address")
 	cmd.Flags().StringVar(&flags.outFile, "out", "",
-		"File where renewed certificate chain is written (required)")
-	cmd.MarkFlagRequired("out")
+		"File where renewed certificate chain is written")
 
 	return cmd
 }
 
-func runRenew(ctx context.Context, csr []byte, srcIA, dstIA addr.IA, trc cppki.SignedTRC,
-	TransportCertFile, TransportKeyFile string, msgr infra.Messenger) ([]*x509.Certificate, error) {
-	chain, err := cppki.ReadPEMCerts(TransportCertFile)
+func loadChain(trc cppki.SignedTRC, file string) ([]*x509.Certificate, addr.IA, error) {
+	chain, err := cppki.ReadPEMCerts(file)
 	if err != nil {
-		return nil, err
-	}
-	if len(chain) == 0 {
-		return nil, serrors.New("no transport certificate was found")
+		return nil, addr.IA{}, err
 	}
 	if err := cppki.VerifyChain(chain, cppki.VerifyOptions{TRC: &trc.TRC}); err != nil {
-		return nil, serrors.WrapStr("verification of transport cert failed with provided TRC", err)
+		return nil, addr.IA{}, serrors.WrapStr(
+			"verification of transport cert failed with provided TRC", err)
+	}
+	ia, err := cppki.ExtractIA(chain[0].Issuer)
+	if err != nil || ia == nil {
+		panic("chain is already validated")
 	}
-	key, err := readECKey(TransportKeyFile)
+	return chain, *ia, nil
+}
+
+func createSigner(srcIA addr.IA, trc cppki.SignedTRC, chain []*x509.Certificate,
+	keyFile string) (trust.Signer, error) {
+
+	key, err := readECKey(keyFile)
 	if err != nil {
-		return nil, err
+		return trust.Signer{}, err
 	}
-	req, err := renewal.NewChainRenewalRequest(ctx, csr, trust.Signer{
+	signer := trust.Signer{
 		PrivateKey:   key,
 		Hash:         crypto.SHA512,
 		IA:           srcIA,
@@ -219,7 +305,14 @@ func runRenew(ctx context.Context, csr []byte, srcIA, dstIA addr.IA, trc cppki.S
 			NotBefore: chain[0].NotBefore,
 			NotAfter:  chain[0].NotAfter,
 		},
-	})
+	}
+	return signer, nil
+}
+
+func renew(ctx context.Context, csr []byte, dstIA addr.IA, signer trust.Signer,
+	msgr infra.Messenger) ([]*x509.Certificate, error) {
+
+	req, err := renewal.NewChainRenewalRequest(ctx, csr, signer)
 	if err != nil {
 		return nil, err
 	}
@@ -238,29 +331,49 @@ func runRenew(ctx context.Context, csr []byte, srcIA, dstIA addr.IA, trc cppki.S
 	return rep.Chain()
 }
 
-func buildCSR(c subjectVars, key *ecdsa.PrivateKey) ([]byte, error) {
+func csrTemplate(chain []*x509.Certificate, tmpl string) (*x509.CertificateRequest, error) {
+	if tmpl == "" {
+		s := chain[0].Subject
+		s.ExtraNames = s.Names
+		return &x509.CertificateRequest{
+			Subject:            s,
+			SignatureAlgorithm: x509.ECDSAWithSHA512,
+		}, nil
+	}
+	vars, err := readVars(tmpl)
+	if err != nil {
+		return nil, serrors.WrapStr("reading template", err)
+	}
+	if vars.ISDAS.IsZero() {
+		return nil, serrors.New("isd_as required in template")
+	}
 	s := pkix.Name{
-		CommonName:         c.CommonName,
-		Country:            []string{c.Country},
-		Organization:       []string{c.Organization},
-		OrganizationalUnit: []string{c.OrganizationalUnit},
-		Locality:           []string{c.Locality},
-		Province:           []string{c.Province},
-		StreetAddress:      []string{c.StreetAddress},
-		PostalCode:         []string{c.PostalCode},
-		SerialNumber:       c.SerialNumber,
+		CommonName:   vars.CommonName,
+		SerialNumber: vars.SerialNumber,
 		ExtraNames: []pkix.AttributeTypeAndValue{
 			{
 				Type:  cppki.OIDNameIA,
-				Value: c.ISDAS,
+				Value: vars.ISDAS.String(),
 			},
 		},
 	}
-	csrTemplate := x509.CertificateRequest{
+	for field, value := range map[*[]string]string{
+		&s.Country:            vars.Country,
+		&s.Organization:       vars.Organization,
+		&s.OrganizationalUnit: vars.OrganizationalUnit,
+		&s.Locality:           vars.Locality,
+		&s.Province:           vars.Province,
+		&s.StreetAddress:      vars.StreetAddress,
+		&s.PostalCode:         vars.PostalCode,
+	} {
+		if value != "" {
+			*field = []string{value}
+		}
+	}
+	return &x509.CertificateRequest{
 		Subject:            s,
 		SignatureAlgorithm: x509.ECDSAWithSHA512,
-	}
-	return x509.CreateCertificateRequest(rand.Reader, &csrTemplate, key)
+	}, nil
 }
 
 func buildMsgr(ctx context.Context, ds reliable.Dispatcher, sds sciond.Service,
@@ -269,7 +382,15 @@ func buildMsgr(ctx context.Context, ds reliable.Dispatcher, sds sciond.Service,
 	if err != nil {
 		return nil, serrors.WrapStr("connecting to SCION Daemon", err)
 	}
-	sn := snet.NewNetwork(local.IA, ds, sciond.RevHandler{Connector: sdConn})
+	sn := &snet.SCIONNetwork{
+		LocalIA: local.IA,
+		Dispatcher: &snet.DefaultPacketDispatcherService{
+			Dispatcher: ds,
+			SCMPHandler: snet.NewSCMPHandler(
+				sciond.RevHandler{Connector: sdConn},
+			),
+		},
+	}
 	conn, err := sn.Dial(ctx, "udp", local.Host, remote, addr.SvcNone)
 	if err != nil {
 		return nil, serrors.WrapStr("dialing", err)
@@ -370,6 +491,15 @@ func findLocalAddr(ctx context.Context, sds sciond.Service) (*snet.UDPAddr, erro
 	}, nil
 }
 
+func outFileFromSubject(renewed []*x509.Certificate, dir string) string {
+	subject, err := cppki.ExtractIA(renewed[0].Subject)
+	if err != nil || subject == nil {
+		panic("chain is already validated")
+	}
+	return filepath.Join(dir, fmt.Sprintf("ISD%d-AS%s.%x.pem", subject.I, subject.A.FileFmt(),
+		renewed[0].SerialNumber.Bytes()))
+}
+
 type svcRouter struct {
 	Connector sciond.Connector
 }


Test patch:
diff --git a/acceptance/cert_renewal_acceptance/test b/acceptance/cert_renewal_acceptance/test
index f1be8dd8a..85857cc5e 100755
--- a/acceptance/cert_renewal_acceptance/test
+++ b/acceptance/cert_renewal_acceptance/test
@@ -132,7 +132,6 @@ class TestRun(CmdBase):
 
         chain = cs_dir / 'crypto/as/renewed.pem'
         args = [
-            '--template', cs_dir / 'crypto/as/csr.json',
             '--key', cs_dir / 'crypto/as/renewed.key',
             '--transportkey', cs_dir / 'crypto/as/cp-as.key',
             '--transportcert', cs_dir / ('crypto/as/ISD%s-AS%s.pem' % (
@@ -140,7 +139,6 @@ class TestRun(CmdBase):
             '--trc', cs_dir / 'certs/ISD1-B1-S1.trc',
             '--out', chain,
             '--sciond', sciond_addr(isd_as),
-            '1-ff00:0:110',
         ]
         if not self.no_docker:
             chain.touch()
diff --git a/go/scion-pki/certs/renew_test.go b/go/scion-pki/certs/renew_test.go
index 636fd3c9c..5518af5dd 100644
--- a/go/scion-pki/certs/renew_test.go
+++ b/go/scion-pki/certs/renew_test.go
@@ -23,9 +23,7 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/asn1"
-	"encoding/json"
 	"fmt"
-	"path/filepath"
 	"testing"
 	"time"
 
@@ -43,65 +41,57 @@ import (
 	"github.com/scionproto/scion/go/lib/sock/reliable"
 	"github.com/scionproto/scion/go/lib/xtest"
 	"github.com/scionproto/scion/go/pkg/trust"
-	"github.com/scionproto/scion/go/proto"
 )
 
-func TestCreateCSR(t *testing.T) {
-	input := `
-{
-	"common_name": "bern",
-	"country": "CH",
-	"organization": "bern",
-	"organizational_unit": "bern InfoSec Squad",
-	"locality": "bern",
-	"isd_as": "1-ff00:0:110"
-}
-`
+func TestCSRTemplate(t *testing.T) {
 	wantSubject := pkix.Name{
-		CommonName:         "bern",
+		CommonName:         "1-ff00:0:111 AS Certificate",
 		Country:            []string{"CH"},
-		Locality:           []string{"bern"},
-		Organization:       []string{"bern"},
-		OrganizationalUnit: []string{"bern InfoSec Squad"},
+		Organization:       []string{"1-ff00:0:111"},
+		OrganizationalUnit: []string{"1-ff00:0:111 InfoSec Squad"},
+		Locality:           []string{"Zürich"},
+		Province:           []string{"Zürich"},
 		ExtraNames: []pkix.AttributeTypeAndValue{
 			{
 				Type:  asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 55324, 1, 2, 1},
-				Value: "1-ff00:0:110",
+				Value: "1-ff00:0:111",
 			},
 		},
 	}
-
-	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	chain, err := cppki.ReadPEMCerts("testdata/renew/ISD1-ASff00_0_111.pem")
 	require.NoError(t, err)
 
 	testCases := map[string]struct {
-		input []byte
-		want  pkix.Name
+		File         string
+		Expected     pkix.Name
+		ErrAssertion assert.ErrorAssertionFunc
 	}{
 		"valid": {
-			input: []byte(input),
-			want:  wantSubject,
+			File:         "testdata/renew/ISD1-ASff00_0_111.csr.json",
+			Expected:     wantSubject,
+			ErrAssertion: assert.NoError,
+		},
+		"from chain": {
+			File:         "",
+			Expected:     wantSubject,
+			ErrAssertion: assert.NoError,
+		},
+		"no ISD-AS": {
+			File:         "testdata/renew/no_isd_as.json",
+			ErrAssertion: assert.Error,
 		},
 	}
-
 	for name, tc := range testCases {
 		name, tc := name, tc
 		t.Run(name, func(t *testing.T) {
 			t.Parallel()
-			n := subjectVars{}
-			require.NoError(t, json.Unmarshal(tc.input, &n))
-
-			csr, err := buildCSR(n, key)
-			require.NoError(t, err)
-
-			got, err := x509.ParseCertificateRequest(csr)
-			require.NoError(t, err)
-			ia, err := cppki.ExtractIA(got.Subject)
-			require.NoError(t, err)
-			assert.Equal(t, "1-ff00:0:110", ia.String())
-			assert.Equal(t, x509.ECDSAWithSHA512, got.SignatureAlgorithm)
-			// TODO(karampok). compare pkixName
-			// assert.Equal(t, tc.want, got.Subject)
+			csr, err := csrTemplate(chain, tc.File)
+			tc.ErrAssertion(t, err)
+			if err != nil {
+				return
+			}
+			assert.Equal(t, x509.ECDSAWithSHA512, csr.SignatureAlgorithm)
+			assert.Equal(t, tc.Expected.String(), csr.Subject.String())
 		})
 	}
 }
@@ -134,59 +124,45 @@ func TestBuildMsgr(t *testing.T) {
 	}
 }
 
-func TestRunRenew(t *testing.T) {
-	goldenDir := "testdata/renew"
-
+func TestRenew(t *testing.T) {
+	trc := xtest.LoadTRC(t, "testdata/renew/ISD1-B1-S1.trc")
 	key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
 	require.NoError(t, err)
-	trc := xtest.LoadTRC(t, filepath.Join(goldenDir, "ISD1-B1-S1.trc"))
-	validSignS := func(msg []byte, rawIA string) *proto.SignS {
-		ia, _ := addr.IAFromString(rawIA)
-		signer := trust.Signer{
-			PrivateKey:   key,
-			Hash:         crypto.SHA512,
-			IA:           ia,
-			TRCID:        trc.TRC.ID,
-			SubjectKeyID: []byte("subject-key-id"),
-			Expiration:   time.Now().Add(20 * time.Hour),
-		}
-		meta, err := signer.Sign(context.Background(), msg)
-		require.NoError(t, err)
-		return meta
-	}
-
-	mctrl := gomock.NewController(t)
-	defer mctrl.Finish()
+	csr := []byte("dummy")
 
 	testCases := map[string]struct {
-		csr                         []byte
-		input                       string
-		localIA, remoteIA           addr.IA
-		msgr                        func() infra.Messenger
-		transportCert, transportKey string
+		Remote addr.IA
+		Msgr   func(t *testing.T, mctrl *gomock.Controller) infra.Messenger
 	}{
 		"valid": {
-			csr:           []byte("dummy"),
-			localIA:       xtest.MustParseIA("1-ff00:0:111"),
-			remoteIA:      xtest.MustParseIA("1-ff00:0:110"),
-			transportCert: "testdata/renew/ISD1-ASff00_0_111.pem",
-			transportKey:  "testdata/renew/cp-as.key",
-			msgr: func() infra.Messenger {
-				c := [][]*x509.Certificate{xtest.LoadChain(t,
-					filepath.Join(goldenDir, "ISD1-ASff00_0_111.pem"))}
+			Remote: xtest.MustParseIA("1-ff00:0:110"),
+			Msgr: func(t *testing.T, mctrl *gomock.Controller) infra.Messenger {
+				c := xtest.LoadChain(t, "testdata/renew/ISD1-ASff00_0_111.pem")
 				raw := []byte{}
-				raw = append(raw, c[0][0].Raw...)
-				raw = append(raw, c[0][1].Raw...)
-				sign := validSignS(raw, "1-ff00:0:111")
+				raw = append(raw, c[0].Raw...)
+				raw = append(raw, c[1].Raw...)
+
+				signer := trust.Signer{
+					PrivateKey:   key,
+					Hash:         crypto.SHA512,
+					IA:           xtest.MustParseIA("1-ff00:0:110"),
+					TRCID:        trc.TRC.ID,
+					SubjectKeyID: []byte("subject-key-id"),
+					Expiration:   time.Now().Add(20 * time.Hour),
+				}
+				meta, err := signer.Sign(context.Background(), raw)
+				require.NoError(t, err)
+
 				rep := &cert_mgmt.ChainRenewalReply{
 					RawChain:  raw,
-					Signature: sign,
+					Signature: meta,
 				}
+
 				// TODO(karampok). Build matchers instead of gomock.Any()
 				// we have to verify that the req is valid signature wise.
 				m := mock_infra.NewMockMessenger(mctrl)
-				m.EXPECT().RequestChainRenewal(ctxMatcher{},
-					gomock.Any(), gomock.Any(), gomock.Any()).Return(rep, nil).Times(1)
+				m.EXPECT().RequestChainRenewal(ctxMatcher{}, gomock.Any(), gomock.Any(),
+					gomock.Any()).Return(rep, nil)
 				return m
 			},
 		},
@@ -196,13 +172,22 @@ func TestRunRenew(t *testing.T) {
 		name, tc := name, tc
 		t.Run(name, func(t *testing.T) {
 			t.Parallel()
-
-			chain, err := runRenew(context.Background(), tc.csr, tc.localIA, tc.remoteIA,
-				trc, tc.transportCert, tc.transportKey, tc.msgr())
+			mctrl := gomock.NewController(t)
+			defer mctrl.Finish()
+
+			signer := trust.Signer{
+				PrivateKey:   key,
+				Hash:         crypto.SHA512,
+				IA:           xtest.MustParseIA("1-ff00:0:111"),
+				TRCID:        trc.TRC.ID,
+				SubjectKeyID: []byte("subject-key-id"),
+				Expiration:   time.Now().Add(20 * time.Hour),
+			}
+
+			chain, err := renew(context.Background(), csr, tc.Remote, signer, tc.Msgr(t, mctrl))
 			require.NoError(t, err)
 			err = cppki.ValidateChain(chain)
 			require.NoError(t, err)
-
 		})
 	}
 }
diff --git a/go/scion-pki/certs/testdata/renew/ISD1-ASff00_0_111.csr.json b/go/scion-pki/certs/testdata/renew/ISD1-ASff00_0_111.csr.json
new file mode 100644
index 000000000..1eae59e97
--- /dev/null
+++ b/go/scion-pki/certs/testdata/renew/ISD1-ASff00_0_111.csr.json
@@ -0,0 +1,9 @@
+{
+    "common_name": "1-ff00:0:111 AS Certificate",
+    "country": "CH",
+    "organization": "1-ff00:0:111",
+    "organizational_unit": "1-ff00:0:111 InfoSec Squad",
+    "locality": "Zürich",
+    "province": "Zürich",
+    "isd_as": "1-ff00:0:111"
+}
diff --git a/go/scion-pki/certs/testdata/renew/no_isd_as.json b/go/scion-pki/certs/testdata/renew/no_isd_as.json
new file mode 100644
index 000000000..8cca251bd
--- /dev/null
+++ b/go/scion-pki/certs/testdata/renew/no_isd_as.json
@@ -0,0 +1,8 @@
+{
+    "common_name": "1-ff00:0:111 AS Certificate",
+    "country": "CH",
+    "organization": "1-ff00:0:111",
+    "organizational_unit": "1-ff00:0:111 InfoSec Squad",
+    "locality": "Zürich",
+    "province": "Zürich"
+}
diff --git a/go/scion-pki/conf/testdata/update_certs.sh b/go/scion-pki/conf/testdata/update_certs.sh
index 03f393f27..373c4fbf1 100755
--- a/go/scion-pki/conf/testdata/update_certs.sh
+++ b/go/scion-pki/conf/testdata/update_certs.sh
@@ -2,7 +2,7 @@
 
 set -ueo pipefail
 
-PLAYGROUND=$(pwd)/../../../../../../scripts/cryptoplayground
+PLAYGROUND=$(pwd)/../../../scripts/cryptoplayground
 PUBDIR="$SAFEDIR"
 KEYDIR="$SAFEDIR"
 
@@ -20,7 +20,7 @@ sed -i \
     -e "s/{{.Location}}/$loc/g" \
     -e "s/{{.Organization}}/$loc/g" \
     -e "s/{{.OrganizationalUnit}}/$loc InfoSec Test Squad/g" \
-    -e "s/{{.ISD-AS}}/$IA/g" \
+    -e "s/{{.ISDAS}}/$IA/g" \
     basic.cnf
 for cnf in *.cnf
 do
diff --git a/go/scion-pki/conf/trc_test.go b/go/scion-pki/conf/trc_test.go
index a3084f8f7..fd1a07e79 100644
--- a/go/scion-pki/conf/trc_test.go
+++ b/go/scion-pki/conf/trc_test.go
@@ -40,7 +40,11 @@ func TestUpdateCerts(t *testing.T) {
 	defer cleanF()
 
 	cmd := exec.Command("sh", "-c", "./testdata/update_certs.sh")
-	cmd.Env = []string{"SAFEDIR=" + dir}
+	cmd.Env = []string{
+		"SAFEDIR=" + dir,
+		"STARTDATE=20200624120000Z",
+		"ENDDATE=20250624120000Z",
+	}
 	out, err := cmd.CombinedOutput()
 	require.NoError(t, err, string(out))
 
@@ -52,6 +56,10 @@ func TestUpdateCerts(t *testing.T) {
 }
 
 func TestLoadTRC(t *testing.T) {
+	if *update {
+		t.Skip("test crypto is being updated")
+	}
+
 	testCases := map[string]struct {
 		file      string
 		cfg       conf.TRC
@@ -77,6 +85,10 @@ func TestLoadTRC(t *testing.T) {
 }
 
 func TestTRCCertificates(t *testing.T) {
+	if *update {
+		t.Skip("test crypto is being updated")
+	}
+
 	rVoting := loadCert(t, "testdata/regular-voting.crt")
 	sVoting := loadCert(t, "testdata/sensitive-voting.crt")
 	testCases := map[string]struct {