Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add intermediate and root verify flags #180

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions cmd/timestamp-cli/app/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ func addVerifyFlags(cmd *cobra.Command) {
cmd.Flags().Var(NewFlagValue(fileFlag, ""), "timestamp", "path to timestamp response to verify")
cmd.MarkFlagRequired("timestamp") //nolint:errcheck
cmd.Flags().Var(NewFlagValue(fileFlag, ""), "certificate-chain", "path to file with PEM-encoded certificate chain. Ordered from intermediate CA certificate that issued the TSA certificate, ending with the root CA certificate.")
cmd.MarkFlagRequired("certificate-chain") //nolint:errcheck
cmd.Flags().String("nonce", "", "optional nonce passed with the request")
cmd.Flags().Var(NewFlagValue(oidFlag, ""), "oid", "optional TSA policy OID passed with the request")
cmd.Flags().String("common-name", "", "expected leaf certificate subject common name")
cmd.Flags().Var(NewFlagValue(fileFlag, ""), "certificate", "path to file with PEM-encoded leaf certificate")
cmd.Flags().Var(NewFlagValue(fileFlag, ""), "intermediate-certificates", "path to file with PEM-encoded intermediate certificates. Must be called with the root-certificate flag.")
cmd.Flags().Var(NewFlagValue(fileFlag, ""), "root-certificates", "path to file with a PEM-encoded root certificates. Must be called with the intermediate-certificates flag.")
malancas marked this conversation as resolved.
Show resolved Hide resolved
}

var verifyCmd = &cobra.Command{
Expand Down Expand Up @@ -144,28 +145,69 @@ func getNonce() (*big.Int, error) {

func getRootAndIntermediateCerts() ([]*x509.Certificate, []*x509.Certificate, error) {
certChainPEM := viper.GetString("certificate-chain")
if certChainPEM == "" {
return nil, nil, nil
rootPEM := viper.GetString("root-certificates")
intermediatePEM := viper.GetString("intermediate-certificates")

// the verify flag must be called with either one of the two flag combinations:
// 1. Called with both the --root-certificates flag and the --intermediate-certificates flag
// 2. Called with only the --certificate-chain flag

// this early exit if statement is only entered if neither of those combinations is valid
if !((intermediatePEM != "" && rootPEM != "" && certChainPEM == "") || (intermediatePEM == "" && rootPEM == "" && certChainPEM != "")) {
malancas marked this conversation as resolved.
Show resolved Hide resolved
return nil, nil, fmt.Errorf("the verify command must be called with either only the --certificate-chain flag or with both the --root-certificate and --intermediate-certificates flags")
}

pemBytes, err := os.ReadFile(filepath.Clean(certChainPEM))
// return root and intermediate certificates when they've been passed
// together with the certificate-chain flag
if certChainPEM != "" {
pemBytes, err := os.ReadFile(filepath.Clean(certChainPEM))
if err != nil {
return nil, nil, fmt.Errorf("error reading request from file: %w", err)
}

certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pemBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse intermediate and root certs from PEM file: %w", err)
}

if len(certs) == 0 {
return nil, nil, fmt.Errorf("expected at least one certificate to represent the root")
}

// intermediate certs are above the root certificate in the PEM file
intermediateCerts := certs[0 : len(certs)-1]
// the root certificate is last in the PEM file
rootCerts := []*x509.Certificate{certs[len(certs)-1]}

return rootCerts, intermediateCerts, nil
}

// return root and intermediate certificates when they've been passed
// separately with the root-certificate and intermediate-certificates flags
rootPEMBytes, err := os.ReadFile(filepath.Clean(rootPEM))
if err != nil {
return nil, nil, fmt.Errorf("error reading request from file: %w", err)
}

certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pemBytes)
rootCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(rootPEMBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse intermediate and root certs from PEM file: %w", err)
}

if len(certs) == 0 {
if len(rootCerts) == 0 {
return nil, nil, fmt.Errorf("expected at least one certificate to represent the root")
}

// intermediate certs are above the root certificate in the PEM file
intermediateCerts := certs[0 : len(certs)-1]
// the root certificate is last in the PEM file
rootCerts := []*x509.Certificate{certs[len(certs)-1]}
// parse intermediate certificates
intermediatePEMBytes, err := os.ReadFile(filepath.Clean(intermediatePEM))
if err != nil {
return nil, nil, fmt.Errorf("error reading request from file: %w", err)
}

intermediateCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(intermediatePEMBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse intermediate and root certs from PEM file: %w", err)
}
malancas marked this conversation as resolved.
Show resolved Hide resolved

return rootCerts, intermediateCerts, nil
}
Expand Down
131 changes: 121 additions & 10 deletions pkg/tests/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestTimestamp(t *testing.T) {
}
}

func TestVerify(t *testing.T) {
func TestVerify_CertificateChainFlag(t *testing.T) {
restapiURL := createServer(t)

artifactContent := "blob"
Expand All @@ -90,14 +90,80 @@ func TestVerify(t *testing.T) {
tsrPath := getTimestamp(t, restapiURL, artifactContent, nonce, policyOID, tsrContainsCerts)

// write the cert chain to a PEM file
_, certChainPemPath := writeCertChainToPEMFiles(t, restapiURL)
pemFiles := writeCertChainToPEMFiles(t, restapiURL)

// It should verify timestamp successfully.
out := runCli(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--certificate-chain", certChainPemPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName)
out := runCli(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--certificate-chain", pemFiles.certChainPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName)
outputContains(t, out, "Successfully verified timestamp")
}

func TestVerifyPassLeafCertificate(t *testing.T) {
func TestVerify_RootAndIntermediateCertificateFlags(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can rewrite all or most of these verify tests as table tests in a follow up PR.

restapiURL := createServer(t)

artifactContent := "blob"
artifactPath := makeArtifact(t, artifactContent)

// this is the common name for the in-memory leaf certificate, copied
// from pkg/signer/memory.go
commonName := "Test TSA Timestamping"
nonce := big.NewInt(456)
policyOID := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 2}
tsrContainsCerts := true

tsrPath := getTimestamp(t, restapiURL, artifactContent, nonce, policyOID, tsrContainsCerts)

// write the cert chain to a PEM file
pemFiles := writeCertChainToPEMFiles(t, restapiURL)

// It should verify timestamp successfully.
out := runCli(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--root-certificates", pemFiles.rootCertsPath, "--intermediate-certificates", pemFiles.intermediateCertsPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName)
outputContains(t, out, "Successfully verified timestamp")
}

func TestVerify_AllCertFlagsIncluded(t *testing.T) {
restapiURL := createServer(t)

artifactContent := "blob"
artifactPath := makeArtifact(t, artifactContent)

// this is the common name for the in-memory leaf certificate, copied
// from pkg/signer/memory.go
commonName := "Test TSA Timestamping"
nonce := big.NewInt(456)
policyOID := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 2}
tsrContainsCerts := true

tsrPath := getTimestamp(t, restapiURL, artifactContent, nonce, policyOID, tsrContainsCerts)

// write the cert chain to a PEM file
pemFiles := writeCertChainToPEMFiles(t, restapiURL)

// It should fail to verify.
out := runCliErr(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--certificate-chain", pemFiles.certChainPath, "--root-certificates", pemFiles.rootCertsPath, "--intermediate-certificates", pemFiles.intermediateCertsPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName)
outputContains(t, out, "the verify command must be called with either only the --certificate-chain flag or with both the --root-certificate and --intermediate-certificates flags")
}

func TestVerify_NoCertFlagsIncluded(t *testing.T) {
restapiURL := createServer(t)

artifactContent := "blob"
artifactPath := makeArtifact(t, artifactContent)

// this is the common name for the in-memory leaf certificate, copied
// from pkg/signer/memory.go
commonName := "Test TSA Timestamping"
nonce := big.NewInt(456)
policyOID := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 2}
tsrContainsCerts := true

tsrPath := getTimestamp(t, restapiURL, artifactContent, nonce, policyOID, tsrContainsCerts)

// It should fail to verify.
out := runCliErr(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName)
outputContains(t, out, "the verify command must be called with either only the --certificate-chain flag or with both the --root-certificate and --intermediate-certificates flags")
}

func TestVerify_PassLeafCertificate(t *testing.T) {
restapiURL := createServer(t)

artifactContent := "blob"
Expand All @@ -113,17 +179,17 @@ func TestVerifyPassLeafCertificate(t *testing.T) {
tsrPath := getTimestamp(t, restapiURL, artifactContent, nonce, policyOID, tsrContainsCerts)

// write the cert chain to a PEM file
leafCertPemPath, certChainPemPath := writeCertChainToPEMFiles(t, restapiURL)
pemFiles := writeCertChainToPEMFiles(t, restapiURL)

// It should verify timestamp successfully.
out := runCli(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--certificate-chain", certChainPemPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName, "--certificate", leafCertPemPath)
out := runCli(t, "--timestamp_server", restapiURL, "verify", "--timestamp", tsrPath, "--artifact", artifactPath, "--certificate-chain", pemFiles.certChainPath, "--nonce", nonce.String(), "--oid", policyOID.String(), "--common-name", commonName, "--certificate", pemFiles.leafCertPath)
outputContains(t, out, "Successfully verified timestamp")
}

func TestVerify_InvalidTSR(t *testing.T) {
restapiURL := createServer(t)

_, pemPath := writeCertChainToPEMFiles(t, restapiURL)
pemFiles := writeCertChainToPEMFiles(t, restapiURL)

artifactContent := "blob"
artifactPath := makeArtifact(t, artifactContent)
Expand All @@ -135,7 +201,7 @@ func TestVerify_InvalidTSR(t *testing.T) {
}

// It should return a message that the PEM is not valid
out := runCliErr(t, "--timestamp_server", restapiURL, "verify", "--timestamp", invalidTSR, "--artifact", artifactPath, "--certificate-chain", pemPath)
out := runCliErr(t, "--timestamp_server", restapiURL, "verify", "--timestamp", invalidTSR, "--artifact", artifactPath, "--certificate-chain", pemFiles.certChainPath)
outputContains(t, out, "error parsing response into Timestamp")
}

Expand Down Expand Up @@ -252,10 +318,17 @@ func getTimestamp(t *testing.T, url string, artifactContent string, nonce *big.I
return path
}

type certChainPEMFiles struct {
leafCertPath string
intermediateCertsPath string
rootCertsPath string
certChainPath string
}

// getCertChainPEM returns the path of a pem file containaing
// the leaf certificate and the path of a pem file containing the
// root and intermediate certificates. Used to verify a signed timestamp
func writeCertChainToPEMFiles(t *testing.T, restapiURL string) (string, string) {
func writeCertChainToPEMFiles(t *testing.T, restapiURL string) certChainPEMFiles {
c, err := client.GetTimestampClient(restapiURL)
if err != nil {
t.Fatalf("unexpected error creating client: %v", err)
Expand Down Expand Up @@ -287,6 +360,39 @@ func writeCertChainToPEMFiles(t *testing.T, restapiURL string) (string, string)
reader := bytes.NewReader(caCertsPEM)
file.ReadFrom(reader)

// create intermediates certificate PEM file
intermediateCertsPath := filepath.Join(t.TempDir(), "ts_intermediates.pem")
file, err = os.Create(intermediateCertsPath)
if err != nil {
t.Fatal(err)
}
defer file.Close()

lastCertIndex := len(certs) - 1
intermediatesPEM, err := cryptoutils.MarshalCertificatesToPEM(certs[1:lastCertIndex])
if err != nil {
t.Fatalf("unexpected error marshalling intermediate certificates: %v", err)
}

reader = bytes.NewReader(intermediatesPEM)
file.ReadFrom(reader)

// create roots certificate PEM file
rootCertsPath := filepath.Join(t.TempDir(), "ts_roots.pem")
file, err = os.Create(rootCertsPath)
if err != nil {
t.Fatal(err)
}
defer file.Close()

rootsPEM, err := cryptoutils.MarshalCertificatesToPEM(certs[lastCertIndex:])
if err != nil {
t.Fatalf("unexpected error marshalling root certificates: %v", err)
}

reader = bytes.NewReader(rootsPEM)
file.ReadFrom(reader)

// create PEM file containing the leaf certificate
leafCertPath := filepath.Join(t.TempDir(), "ts_leafcert.pem")
file, err = os.Create(leafCertPath)
Expand All @@ -303,7 +409,12 @@ func writeCertChainToPEMFiles(t *testing.T, restapiURL string) (string, string)
reader = bytes.NewReader(leafCertPEM)
file.ReadFrom(reader)

return leafCertPath, certChainPath
return certChainPEMFiles{
leafCertPath: leafCertPath,
intermediateCertsPath: intermediateCertsPath,
rootCertsPath: rootCertsPath,
certChainPath: certChainPath,
}
}

// Create a random artifact to sign
Expand Down