Files
certigo/jceks/keytool_test.go
Nik 3053a19816 Add JCEKS integration tests (#350)
This commit adds tests for the `jceks` package against the
installed JDK's `keytool` CLI. The tests ensure that JCEKS files
created by `keytool` can be read by the package, and vice versa.
These tests are disabled by default and are enabled with the
`-jceks.keytool-tests=true` flag. This commit configures GitHub
Actions to test the package against a wide variety of JDKs.

This commit also restricts GitHub Actions permissions.
2025-08-07 17:33:08 -04:00

244 lines
7.2 KiB
Go

// Copyright 2025 Block, Inc.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jceks
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag"
"math/rand/v2"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/sha3"
)
var keytoolTests = flag.Bool("jceks.keytool-tests", false, "run Keytool integration tests")
// TestIntegrationKeytoolPackageRead tests that a JCEKS file written by keytool can be read by this package.
func TestIntegrationKeytoolPackageRead(t *testing.T) {
t.Parallel()
if !*keytoolTests {
t.Skip("keytool integration tests are not enabled")
}
p12Filename := filepath.Join(os.TempDir(), "jceks-test-"+strconv.FormatUint(rand.Uint64(), 16)+".p12")
jceksFilename := filepath.Join(os.TempDir(), "jceks-test-"+strconv.FormatUint(rand.Uint64(), 16)+".jceks")
defer func() {
_ = os.Remove(p12Filename)
_ = os.Remove(jceksFilename)
}()
// Generate a JCEKS file by using openssl to make a PKCS#12 file and then converting it to JCEKS with keytool
err := exec.Command("openssl", "pkcs12",
"-export",
"-in", "testdata/private-key.crt",
"-inkey", "testdata/private-key.key",
"-certfile", "testdata/private-key-ca.crt",
"-name", "test-private-key",
"-passout", "pass:store-password",
"-out", p12Filename,
).Run()
require.NoError(t, err)
err = exec.Command("keytool", "-importkeystore",
"-alias", "test-private-key",
"-srckeystore", p12Filename,
"-srcstoretype", "PKCS12",
"-srcstorepass", "store-password",
"-destkeystore", jceksFilename,
"-storetype", "JCEKS",
"-deststorepass", "store-password",
"-destkeypass", "key-password",
).Run()
require.NoError(t, err)
err = exec.Command("keytool", "-importcert",
"-noprompt",
"-alias", "test-trusted-cert",
"-file", "testdata/trusted-cert.crt",
"-destkeystore", jceksFilename,
"-storetype", "JCEKS",
"-deststorepass", "store-password",
).Run()
require.NoError(t, err)
// Now load the JCEKS file with the package and make sure that the entries match the source material
ks, err := LoadFromFile(jceksFilename, []byte("store-password"))
require.NoError(t, err)
privKey, certs, err := ks.GetPrivateKeyAndCerts("test-private-key", []byte("key-password"))
require.NoError(t, err)
require.NotNil(t, privKey)
require.IsType(t, &rsa.PrivateKey{}, privKey)
rsaKey := privKey.(*rsa.PrivateKey)
expectedRSAKey, err := LoadPEMKey("testdata/private-key.key")
require.NoError(t, err)
require.Equal(t, expectedRSAKey, rsaKey)
require.True(t, rsaKey.Equal(expectedRSAKey))
expectedLeafCert, err := LoadPEMCert("testdata/private-key.crt")
require.NoError(t, err)
expectedCACert, err := LoadPEMCert("testdata/private-key-ca.crt")
require.NoError(t, err)
require.Len(t, certs, 2)
require.True(t, certs[0].Equal(expectedLeafCert))
require.True(t, certs[1].Equal(expectedCACert))
keyAliases := ks.ListPrivateKeys()
require.Equal(t, []string{"test-private-key"}, keyAliases)
cert, err := ks.GetCert("test-trusted-cert")
require.NoError(t, err)
expectedCert, err := LoadPEMCert("testdata/trusted-cert.crt")
require.NoError(t, err)
require.True(t, cert.Equal(expectedCert))
certAliases := ks.ListCerts()
require.Equal(t, []string{"test-trusted-cert"}, certAliases)
}
// TestIntegrationKeytoolPackageWrite tests that a JCEKS file written by this package can be read by keytool.
func TestIntegrationKeytoolPackageWrite(t *testing.T) {
t.Parallel()
if !*keytoolTests {
t.Skip("keytool integration tests are not enabled")
}
rnd := sha3.NewShake128()
now := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
p12Filename := filepath.Join(os.TempDir(), "jceks-test-"+strconv.FormatUint(rand.Uint64(), 16)+".p12")
jceksFilename := filepath.Join(os.TempDir(), "jceks-test-"+strconv.FormatUint(rand.Uint64(), 16)+".jceks")
pemFilename := filepath.Join(os.TempDir(), "jceks-test-"+strconv.FormatUint(rand.Uint64(), 16)+".pem")
defer func() {
_ = os.Remove(p12Filename)
_ = os.Remove(jceksFilename)
_ = os.Remove(pemFilename)
}()
// Create a JCEKS file from source material using this package
rsaKey, err := LoadPEMKey("testdata/private-key.key")
require.NoError(t, err)
leafCert, err := LoadPEMCert("testdata/private-key.crt")
require.NoError(t, err)
caCert, err := LoadPEMCert("testdata/private-key-ca.crt")
require.NoError(t, err)
cert, err := LoadPEMCert("testdata/trusted-cert.crt")
require.NoError(t, err)
var enc Encoder
err = enc.SetIntegrityPassword("store-password")
require.NoError(t, err)
pkcs1 := x509.MarshalPKCS1PrivateKey(rsaKey)
cipher, err := PBEWithMD5AndDES3CBC([]byte("key-password"), rnd, 20)
require.NoError(t, err)
err = enc.AddPrivateKeyPKCS1("test-private-key", now, pkcs1, [][]byte{leafCert.Raw, caCert.Raw}, cipher)
require.NoError(t, err)
err = enc.AddTrustedCertificate("test-trusted-cert", now, cert.Raw)
require.NoError(t, err)
f, err := os.Create(jceksFilename)
require.NoError(t, err)
defer func() {
_ = f.Close()
}()
_, err = enc.WriteTo(f)
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
// Now convert the JCEKS file to PKCS#12 with keytool, and then use openssl to extract the certs and keys into PEM
err = exec.Command("keytool", "-importkeystore",
"-srckeystore", jceksFilename,
"-srcstoretype", "JCEKS",
"-srcstorepass", "store-password",
"-destkeystore", p12Filename,
"-storetype", "PKCS12",
"-deststorepass", "store-password",
"-alias", "test-private-key",
"-srckeypass", "key-password",
"-destkeypass", "store-password",
).Run()
require.NoError(t, err)
err = exec.Command("keytool", "-importkeystore",
"-srckeystore", jceksFilename,
"-srcstoretype", "JCEKS",
"-srcstorepass", "store-password",
"-destkeystore", p12Filename,
"-storetype", "PKCS12",
"-deststorepass", "store-password",
"-alias", "test-trusted-cert",
).Run()
require.NoError(t, err)
err = exec.Command("openssl", "pkcs12",
"-in", p12Filename,
"-out", pemFilename,
"-nodes",
"-passin", "pass:store-password",
"-legacy",
).Run()
require.NoError(t, err)
// Finally, verify that the material that ended up in the PEM matches the source material
pemData, err := os.ReadFile(pemFilename)
require.NoError(t, err)
var certsDER [][]byte
var keysDER [][]byte
for {
var pemBlock *pem.Block
pemBlock, pemData = pem.Decode(pemData)
if pemBlock == nil {
break
}
switch pemBlock.Type {
case "CERTIFICATE":
certsDER = append(certsDER, pemBlock.Bytes)
case "PRIVATE KEY":
keysDER = append(keysDER, pemBlock.Bytes)
}
}
require.ElementsMatch(t, certsDER, [][]byte{leafCert.Raw, caCert.Raw, cert.Raw})
pkcs8, err := x509.MarshalPKCS8PrivateKey(rsaKey)
require.NoError(t, err)
require.ElementsMatch(t, keysDER, [][]byte{pkcs8})
}