server/signedcontainer/verify.go (128 lines of code) (raw):
// Package signedcontainer contains functions to verify container image signatures.
package signedcontainer
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"sync"
"github.com/GoogleCloudPlatform/confidential-space/server/signedcontainer/internal/convert"
"github.com/tink-crypto/tink-go/v2/keyset"
tinksig "github.com/tink-crypto/tink-go/v2/signature"
)
// ImageSignature represents a container image signature.
type ImageSignature struct {
Payload []byte
Signature []byte
}
const maxSignatureCount = 300
// VerifiedSignature contains information about a successfully verified signature.
type VerifiedSignature struct {
KeyID string `json:"key_id,omitempty"`
Signature string `json:"signature,omitempty"`
Alg string `json:"signature_algorithm,omitempty"`
}
// VerifyResult contains the results of verifying a list of signatures.
type VerifyResult struct {
Verified []*VerifiedSignature
Errors []error
}
// Verify attempts to verify the provided signatures with imageDigest and returns a VerifyResults
// object, which contains successfully verified signatures and the errors that arose from verification errors.
func Verify(imageDigest string, signatures []*ImageSignature) (*VerifyResult, error) {
numSignatures := len(signatures)
if numSignatures == 0 {
return &VerifyResult{}, nil
} else if numSignatures > maxSignatureCount {
return &VerifyResult{}, fmt.Errorf("got %v signatures, should be less than the limit %d", numSignatures, maxSignatureCount)
}
validSigs := make([]*VerifiedSignature, numSignatures)
validationErrs := make([]error, numSignatures)
// Perform signature verification.
var wg sync.WaitGroup
for i, sig := range signatures {
wg.Add(1)
go func(index int, s *ImageSignature) {
defer wg.Done()
verified, err := verifySignature(imageDigest, s)
if err != nil {
validationErrs[index] = err
} else {
validSigs[index] = verified
}
}(i, sig)
}
wg.Wait()
var sigs []*VerifiedSignature
for _, sig := range validSigs {
if sig != nil {
sigs = append(sigs, sig)
}
}
var errs []error
for _, err := range validationErrs {
if err != nil {
errs = append(errs, err)
}
}
return &VerifyResult{sigs, errs}, nil
}
var encoding = base64.StdEncoding
// computeKeyID computes a hexadecimal fingerprint for a public key using SHA256.
// This will generate a keyID that aligns with this openssl command:
// openssl pkey -pubin -in public_key.pem -outform DER | openssl sha256
func computeKeyID(pemBytes []byte) (string, error) {
derBlock, rest := pem.Decode(pemBytes)
if derBlock == nil {
return "", errors.New("could not decode public key bytes as PEM")
}
if len(rest) > 0 {
return "", errors.New("unexpected trailing data in key file")
}
// Use sha256 to compute the fingerprint on the DER bytes.
fingerprint := sha256.Sum256(derBlock.Bytes)
return hex.EncodeToString(fingerprint[:]), nil
}
// verifySignature performs the following operations to verify a container image signature:
// 1. Parses the signature payload to get the attached public key and signing algorithm.
// 2. Verifies if payload contains the expected workload image digest.
// 3. Verifies if the given container image signature is valid using Tink and returns error if the signature verification failed.
func verifySignature(imageDigest string, sig *ImageSignature) (*VerifiedSignature, error) {
if sig == nil {
return nil, errors.New("container image signature is nil")
}
payload, err := unmarshalAndValidate(sig.Payload)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal payload: %v", err)
}
publicKey, err := payload.publicKey()
if err != nil {
return nil, err
}
sigAlg, err := payload.sigAlg()
if err != nil {
return nil, err
}
if payload.Critical.Image.DockerManifestDigest != imageDigest {
return nil, errors.New("payload docker manifest digest does not match the running workload image digest")
}
// Create a public keyset handle from the given PEM-encoded public key and signing algorithm.
publicKeysetHandle, err := createPublicKeysetHandle(publicKey, sigAlg)
if err != nil {
return nil, fmt.Errorf("failed to read public keyset: %v", err)
}
// Retrieve the Verifier primitive from publicKeysetHandle.
verifier, err := tinksig.NewVerifier(publicKeysetHandle)
if err != nil {
return nil, fmt.Errorf("failed to create Tink signature verifier: %v", err)
}
if err = verifier.Verify(sig.Signature, sig.Payload); err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
keyID, err := computeKeyID(publicKey)
if err != nil {
return nil, fmt.Errorf("failed to compute keyID: %v", err)
}
return &VerifiedSignature{
KeyID: keyID,
Signature: encoding.EncodeToString(sig.Signature),
Alg: sigAlg.string(),
}, nil
}
// createPublicKeysetHandle takes in the given PEM-encoded public key and creates a public keyset handle based on the signing algorithm.
func createPublicKeysetHandle(publicKey []byte, sigAlg signingAlgorithm) (*keyset.Handle, error) {
switch sigAlg {
case ecdsaP256Sha256:
return convert.PemToECDSAP256Sha256WithDEREncodingKeysetHandle(publicKey)
case rsasaaPkcs1v15Sha256:
return convert.PemToRsaSsaPkcs1Sha256KeysetHandle(publicKey)
case rsassaPssSha256:
return convert.PemToRsaSsaPssSha256KeysetHandle(publicKey)
default:
return nil, fmt.Errorf("unsupported signing algorithm: %v", sigAlg)
}
}