diff --git a/src/main.go b/src/main.go index b04693d..54190cd 100644 --- a/src/main.go +++ b/src/main.go @@ -1,8 +1,16 @@ package main import ( + "bytes" "context" + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" "encoding/json" + "encoding/pem" "errors" "flag" "fmt" @@ -84,79 +92,86 @@ func checkEFI(expected_loader_entry string) error { return nil } -func getManifest(repo *remote.Repository, ctx context.Context, ref string) (map[string]interface{}, error) { - manifest_descriptor, err := repo.Resolve(ctx, ref) +func getManifestBytes(repo *remote.Repository, ctx context.Context, ref string) ([]byte, error) { + manifestDescriptor, err := repo.Resolve(ctx, ref) if err != nil { return nil, err } + manifestStream, err := repo.Fetch(ctx, manifestDescriptor) + if err != nil { + return nil, err + } + defer manifestStream.Close() - mainfest_stream, err := repo.Fetch(ctx, manifest_descriptor) + manifestContent, err := io.ReadAll(manifestStream) if err != nil { return nil, err } - defer mainfest_stream.Close() - var manifest map[string]interface{} + return manifestContent, nil +} - manifest_content, err := io.ReadAll(mainfest_stream) +func getBlobBytes(repo *remote.Repository, ctx context.Context, ref string) ([]byte, error) { + manifestDescriptor, err := repo.Blobs().Resolve(ctx, ref) + if err != nil { + return nil, err + } + manifestStream, err := repo.Fetch(ctx, manifestDescriptor) if err != nil { return nil, err } + defer manifestStream.Close() - err = json.Unmarshal(manifest_content, &manifest) + blobContent, err := io.ReadAll(manifestStream) if err != nil { return nil, err } - return manifest, nil + return blobContent, nil } func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag string, cname string) (string, error) { - manifest, err := getManifest(repo, ctx, tag) + indexData, err := getManifestBytes(repo, ctx, tag) if err != nil { return "", err } - var digest string + index := Index{} + err = json.Unmarshal(indexData, &index) + if err != nil { + return "", err + } - for _, entry := range manifest["manifests"].([]interface{}) { - item := entry.(map[string]interface{}) - item_digest := item["digest"].(string) - item_annotations := item["annotations"].(map[string]interface{}) - item_cname := item_annotations["cname"].(string) + var digest string - if strings.HasPrefix(item_cname, cname) { - digest = item_digest - break + for _, entry := range index.Manifests { + if strings.HasPrefix(entry.Annotations.Cname, cname) { + digest = entry.Digest + return digest, nil } } - - return digest, nil + return "", errors.New("no manifest found for cname " + cname) } func getLayerByMediaType(repo *remote.Repository, ctx context.Context, digest string, media_type string) (string, uint64, error) { - manifest, err := getManifest(repo, ctx, digest) + manifestData, err := getManifestBytes(repo, ctx, digest) if err != nil { return "", 0, err } - var layer string - var size uint64 - - for _, entry := range manifest["layers"].([]interface{}) { - item := entry.(map[string]interface{}) - item_digest := item["digest"].(string) - item_size := uint64(item["size"].(float64)) - item_media_type := item["mediaType"].(string) + manifest := Manifest{} + err = json.Unmarshal(manifestData, &manifest) + if err != nil { + return "", 0, err + } - if item_media_type == media_type { - layer = item_digest - size = item_size - break + for _, layer := range manifest.Layers { + if media_type == layer.MediaType { + return layer.Digest, layer.Size, nil } } - return layer, size, nil + return "", 0, errors.New("no layer found for media type " + media_type) } func getFilesWithPrefix(dir string, prefix string) ([]string, error) { @@ -284,9 +299,105 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e return nil } -const ERR_INVALID_ARGUMENTS = 1 -const ERR_SYSTEM_FAILURE = 2 -const ERR_NETWORK_PROBLEMS = 3 +func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verificationKeyFile string) { + signatureTag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" + signatureManifestBytes, err := getManifestBytes(repo, ctx, signatureTag) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_NETWORK_PROBLEMS) + } + + signatureManifest := SignatureManifest{} + err = json.Unmarshal(signatureManifestBytes, &signatureManifest) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + // types + signatureStr := signatureManifest.Layers[0].Annotations.Signature + signature, err := base64.StdEncoding.DecodeString(signatureStr) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + + messageHashStr := signatureManifest.Layers[0].Digest + messageHashFromManifest, err := hex.DecodeString(strings.Trim(messageHashStr, "sha256:")) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + + // Here we pull the messageHashStr. This is insufficient for a proper signature verification. We have to + // check the messages contents, validate that it contains the correct manifest digest, and then hash it ourselves. + + // 1. Get signed message + message, err := getBlobBytes(repo, ctx, messageHashStr) + if err != nil { + fmt.Fprintln(os.Stderr, "Error fetching signed message:", err) + os.Exit(ERR_NETWORK_PROBLEMS) + } + signatureMessage := SignatureMessage{} + err = json.Unmarshal(message, &signatureMessage) + if err != nil { + fmt.Fprintln(os.Stderr, "Error unmarshalling signature message:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + // 2. Check if correct digest is in the signed message + if digest != signatureMessage.Critical.Image.DockerManifestDigest { + fmt.Fprintln(os.Stderr, "Error during signature verification, the digest of the manifest to be verified ", digest, " is not equal to the digest that is in the signed message ", signatureMessage.Critical.Image.DockerManifestDigest) + os.Exit(ERR_SYSTEM_FAILURE) + } + + // 3. hash the signature message + local_hash := sha256.Sum256(message) + // 4. check if hash in signaturemanifest == locally computed hash of the message + if !bytes.Equal(local_hash[:], messageHashFromManifest) { + fmt.Fprintln(os.Stderr, "Error: the locally computed digest of the signed message (", + messageHashFromManifest, "), does not match the digest from the signature manifest (", + messageHashStr) + os.Exit(ERR_SYSTEM_FAILURE) + } + + pubKey := getVerificationKey(verificationKeyFile) + + err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, local_hash[:], signature) + if err == nil { + fmt.Println("Verified OK") + } else { + fmt.Fprintln(os.Stderr, "Invalid signature:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + +} + +func getVerificationKey(verificationKeyFile string) *rsa.PublicKey { + keyData, err := os.ReadFile(verificationKeyFile) + if err != nil { + fmt.Fprintln(os.Stderr, "Error loading key:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + block, _ := pem.Decode(keyData) + if block == nil { + fmt.Fprintln(os.Stderr, "Error decoding pemdata.") + os.Exit(ERR_SYSTEM_FAILURE) + } + + pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + fmt.Fprintln(os.Stderr, "Error parsing key:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + return pubKey.(*rsa.PublicKey) +} + +// Error codes should represent whether it is worth to retry (network errors for example) or not to retry (invalid arguments) +const ( + _ = iota + ERR_INVALID_ARGUMENTS // permanent + ERR_SYSTEM_FAILURE // permanent + ERR_NETWORK_PROBLEMS // retry makes sense +) func main() { flag_set := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) @@ -297,6 +408,7 @@ func main() { target_dir := flag_set.String("target-dir", "/efi/EFI/Linux", "directory to write artifacts to") os_release_path := flag_set.String("os-release", "/etc/os-release", "alternative path where the os-release file is read from") skip_efi_check := flag_set.Bool("skip-efi-check", false, "skip performing EFI safety checks") + verification_key_file := flag_set.String("verification-key", "/etc/gardenlinux/oci_signing_key.pem", "path to verification key file") flag_set.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) @@ -342,11 +454,18 @@ func main() { os.Exit(ERR_NETWORK_PROBLEMS) } + // verify the signature here + verifyManifest(repo, ctx, digest, *verification_key_file) + layer, size, err := getLayerByMediaType(repo, ctx, digest, *media_type) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(ERR_NETWORK_PROBLEMS) } + if layer == "" || size == 0 { + fmt.Fprintln(os.Stderr, "No layer found for "+cname+" version: "+version+" and mediatype"+*media_type+" on "+*repo_url) + os.Exit(ERR_SYSTEM_FAILURE) + } space_required := size + (1024 * 1024) @@ -354,7 +473,7 @@ func main() { space, err := getAvailableSpace(*target_dir) if err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) + fmt.Fprintln(os.Stderr, "Error checking available space:", err) os.Exit(ERR_SYSTEM_FAILURE) } @@ -362,7 +481,7 @@ func main() { space_wanted := space_required - space err := garbageClean(*target_dir, cname, current_version, int64(space_wanted)) if err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) + fmt.Fprintln(os.Stderr, "Error cleaning up:", err) os.Exit(ERR_SYSTEM_FAILURE) } } diff --git a/src/types.go b/src/types.go new file mode 100644 index 0000000..d53905d --- /dev/null +++ b/src/types.go @@ -0,0 +1,53 @@ +package main + +type SignatureManifest struct { + Layers []struct { + Digest string `json:"digest"` + Annotations struct { + Signature string `json:"dev.cosignproject.cosign/signature"` + } `json:"annotations"` + } `json:"layers"` +} + +type SignatureMessage struct { + Critical struct { + Identity struct { + DockerReference string `json:"docker-reference"` + } `json:"identity"` + Image struct { + DockerManifestDigest string `json:"docker-manifest-digest"` + } `json:"image"` + Type string `json:"type"` + } `json:"critical"` +} + +type Index struct { + Manifests []struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int `json:"size"` + Platform struct { + Architecture string `json:"architecture"` + Os string `json:"os"` + } `json:"platform"` + Annotations struct { + Cname string `json:"cname"` + Architecture string `json:"architecture"` + FeatureSet string `json:"feature_set"` + } `json:"annotations,omitempty"` + } `json:"manifests"` +} + +type Manifest struct { + MediaType string `json:"mediaType"` + Config struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int `json:"size"` + } `json:"config"` + Layers []struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size uint64 `json:"size"` + } `json:"layers"` +}