Skip to content

Commit

Permalink
Merge pull request #1810 from mtrmac/generate-sigstore-key
Browse files Browse the repository at this point in the history
Add signature/sigstore.GenerateKeyPair
  • Loading branch information
mtrmac authored Jan 23, 2023
2 parents c42c14f + 187ad35 commit ed23a00
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 0 deletions.
29 changes: 29 additions & 0 deletions signature/sigstore/copied.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
35 changes: 35 additions & 0 deletions signature/sigstore/generate.go
Original file line number Diff line number Diff line change
@@ -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
}
64 changes: 64 additions & 0 deletions signature/sigstore/generate_test.go
Original file line number Diff line number Diff line change
@@ -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.
}

0 comments on commit ed23a00

Please sign in to comment.