From 187ad354eaba33cb7d0fe21c6d1f70021755c641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 21 Jan 2023 01:29:23 +0100 Subject: [PATCH] Add signature/sigstore.GenerateKeyPair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses the original cosign private key format, but the API allows us to change our mind in the future (e.g. to something using FIPS-acceptable algorithms). Signed-off-by: Miloslav Trmač --- signature/sigstore/copied.go | 29 +++++++++++++ signature/sigstore/generate.go | 35 ++++++++++++++++ signature/sigstore/generate_test.go | 64 +++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 signature/sigstore/generate.go create mode 100644 signature/sigstore/generate_test.go diff --git a/signature/sigstore/copied.go b/signature/sigstore/copied.go index dbc03ec0a..0233c4cb8 100644 --- a/signature/sigstore/copied.go +++ b/signature/sigstore/copied.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/theupdateframework/go-tuf/encrypted" ) @@ -68,3 +69,31 @@ func loadPrivateKey(key []byte, pass []byte) (signature.SignerVerifier, error) { return nil, errors.New("unsupported key type") } } + +// simplified from sigstore/cosign/pkg/cosign.marshalKeyPair +// loadPrivateKey always requires a encryption, so this always requires a passphrase. +func marshalKeyPair(privateKey crypto.PrivateKey, publicKey crypto.PublicKey, password []byte) (_privateKey []byte, _publicKey []byte, err error) { + x509Encoded, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, nil, fmt.Errorf("x509 encoding private key: %w", err) + } + + encBytes, err := encrypted.Encrypt(x509Encoded, password) + if err != nil { + return nil, nil, err + } + + // store in PEM format + privBytes := pem.EncodeToMemory(&pem.Block{ + Bytes: encBytes, + Type: sigstorePrivateKeyPemType, + }) + + // Now do the public key + pubBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey) + if err != nil { + return nil, nil, err + } + + return privBytes, pubBytes, nil +} diff --git a/signature/sigstore/generate.go b/signature/sigstore/generate.go new file mode 100644 index 000000000..77520c123 --- /dev/null +++ b/signature/sigstore/generate.go @@ -0,0 +1,35 @@ +package sigstore + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" +) + +// GenerateKeyPairResult is a struct to ensure the private and public parts can not be confused by the caller. +type GenerateKeyPairResult struct { + PublicKey []byte + PrivateKey []byte +} + +// GenerateKeyPair generates a public/private key pair usable for signing images using the sigstore format, +// and returns key representations suitable for storing in long-term files (with the private key encrypted using the provided passphrase). +// The specific key kind (e.g. algorithm, size), as well as the file format, are unspecified by this API, +// and can change with best practices over time. +func GenerateKeyPair(passphrase []byte) (*GenerateKeyPairResult, error) { + // https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#signature-schemes + // only requires ECDSA-P256 to be supported, so that’s what we must use. + rawKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + // Coverage: This can fail only if the randomness source fails + return nil, err + } + private, public, err := marshalKeyPair(rawKey, rawKey.Public(), passphrase) + if err != nil { + return nil, err + } + return &GenerateKeyPairResult{ + PublicKey: public, + PrivateKey: private, + }, nil +} diff --git a/signature/sigstore/generate_test.go b/signature/sigstore/generate_test.go new file mode 100644 index 000000000..565727ab6 --- /dev/null +++ b/signature/sigstore/generate_test.go @@ -0,0 +1,64 @@ +package sigstore + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/signature/internal" + "github.com/opencontainers/go-digest" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateKeyPair(t *testing.T) { + // Test that generation is possible, and the key can be used for signing. + testManifest := []byte("{}") + testDockerReference, err := reference.ParseNormalizedNamed("example.com/foo:notlatest") + require.NoError(t, err) + + passphrase := []byte("some passphrase") + keyPair, err := GenerateKeyPair(passphrase) + require.NoError(t, err) + + tmpDir := t.TempDir() + privateKeyFile := filepath.Join(tmpDir, "private.key") + err = os.WriteFile(privateKeyFile, keyPair.PrivateKey, 0600) + require.NoError(t, err) + + signer, err := NewSigner(WithPrivateKeyFile(privateKeyFile, passphrase)) + require.NoError(t, err) + sig_, err := internalSigner.SignImageManifest(context.Background(), signer, testManifest, testDockerReference) + require.NoError(t, err) + sig, ok := sig_.(signature.Sigstore) + require.True(t, ok) + + // It would be even more elegant to invoke the higher-level prSigstoreSigned code, + // but that is private. + publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(keyPair.PublicKey) + require.NoError(t, err) + + _, err = internal.VerifySigstorePayload(publicKey, sig.UntrustedPayload(), + sig.UntrustedAnnotations()[signature.SigstoreSignatureAnnotationKey], + internal.SigstorePayloadAcceptanceRules{ + ValidateSignedDockerReference: func(ref string) error { + assert.Equal(t, "example.com/foo:notlatest", ref) + return nil + }, + ValidateSignedDockerManifestDigest: func(digest digest.Digest) error { + matches, err := manifest.MatchesDigest(testManifest, digest) + require.NoError(t, err) + assert.True(t, matches) + return nil + }, + }) + assert.NoError(t, err) + + // The failure paths are not obviously easy to reach. +}