From 7dc347b27c624dd6884cdbea1b907dc7fdb6832a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Tue, 3 Dec 2024 16:37:30 +0100 Subject: [PATCH 01/14] introduce verification function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main.go b/src/main.go index b04693d..cb6fc5e 100644 --- a/src/main.go +++ b/src/main.go @@ -284,6 +284,17 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e return nil } +func verify_manifest(repo *remote.Repository, ctx context.Context, digest string) { + signature_tag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" + signature_manifest, err := getManifest(repo, ctx, signature_tag) + if err != nil { + panic(err) + } + fmt.Println(signature_manifest["layers"].([]interface{})[0].(map[string]interface{})) + panic("bye") + +} + const ERR_INVALID_ARGUMENTS = 1 const ERR_SYSTEM_FAILURE = 2 const ERR_NETWORK_PROBLEMS = 3 @@ -342,6 +353,9 @@ func main() { os.Exit(ERR_NETWORK_PROBLEMS) } + // verify the signature here + verify_manifest(repo, ctx, digest) + layer, size, err := getLayerByMediaType(repo, ctx, digest, *media_type) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) From 1ece778c3dbe7fd4df333fb5e605a2776a8255e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Thu, 28 Nov 2024 15:17:23 +0100 Subject: [PATCH 02/14] Deal with normal container entries, rename manifest to index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main.go b/src/main.go index cb6fc5e..f6a337a 100644 --- a/src/main.go +++ b/src/main.go @@ -112,16 +112,20 @@ func getManifest(repo *remote.Repository, ctx context.Context, ref string) (map[ } func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag string, cname string) (string, error) { - manifest, err := getManifest(repo, ctx, tag) + index, err := getManifest(repo, ctx, tag) if err != nil { return "", err } var digest string - for _, entry := range manifest["manifests"].([]interface{}) { + for _, entry := range index["manifests"].([]interface{}) { item := entry.(map[string]interface{}) item_digest := item["digest"].(string) + // annotations only exist for gardenlinux artifacts, "normal" container runtime entries do not have that field + if item["annotations"] == nil { + continue + } item_annotations := item["annotations"].(map[string]interface{}) item_cname := item_annotations["cname"].(string) @@ -361,6 +365,10 @@ func main() { fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(ERR_NETWORK_PROBLEMS) } + if layer == "" || size == 0 { + fmt.Println(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) From 0c86ac0a8abdeebca4a7e324446b3a12967aed23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Tue, 3 Dec 2024 17:20:07 +0100 Subject: [PATCH 03/14] intermediate state, verification works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/main.go b/src/main.go index f6a337a..d22f327 100644 --- a/src/main.go +++ b/src/main.go @@ -2,7 +2,13 @@ package main import ( "context" + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/hex" "encoding/json" + "encoding/pem" "errors" "flag" "fmt" @@ -288,13 +294,49 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e return nil } -func verify_manifest(repo *remote.Repository, ctx context.Context, digest string) { - signature_tag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" - signature_manifest, err := getManifest(repo, ctx, signature_tag) +func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) { + signatureTag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" + signatureManifest, err := getManifest(repo, ctx, signatureTag) if err != nil { panic(err) } - fmt.Println(signature_manifest["layers"].([]interface{})[0].(map[string]interface{})) + signatureStr := signatureManifest["layers"].([]interface{})[0].(map[string]interface{})["annotations"].(map[string]interface{})["dev.cosignproject.cosign/signature"].(string) + signature, err := base64.StdEncoding.DecodeString(signatureStr) + + messageHashStr := strings.Trim(signatureManifest["layers"].([]interface{})[0].(map[string]interface{})["digest"].(string), "sha256:") + messageHash, err := hex.DecodeString(messageHashStr) + if err != nil { + fmt.Printf("Error decoding messageHashStr: %s\n", err) + } + fmt.Println(signature) + fmt.Println(messageHash) + + // Key -> hardcode / read from disk + keyData, err := os.ReadFile("key") + if err != nil { + panic(err) + } + fmt.Println("Keyfile loaded.") + + block, _ := pem.Decode(keyData) + if block == nil { + panic("unable to PEM decode provided key") + } + fmt.Println("PEM data loaded.") + + pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + panic(err) + } + fmt.Println("Key loaded.") + + err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, messageHash[:], signature) + if err == nil { + fmt.Println("Verified OK") + } else { + panic(err) + } + panic("bye") } @@ -358,7 +400,7 @@ func main() { } // verify the signature here - verify_manifest(repo, ctx, digest) + verifyManifest(repo, ctx, digest) layer, size, err := getLayerByMediaType(repo, ctx, digest, *media_type) if err != nil { @@ -366,7 +408,7 @@ func main() { os.Exit(ERR_NETWORK_PROBLEMS) } if layer == "" || size == 0 { - fmt.Println(os.Stderr, "No layer found for " + cname + " version: " + version + " and mediatype" + *media_type + " on " + *repo_url) + fmt.Println(os.Stderr, "No layer found for "+cname+" version: "+version+" and mediatype"+*media_type+" on "+*repo_url) os.Exit(ERR_SYSTEM_FAILURE) } From 4d487ed24d143e65423d68bb2bb42f16096ca79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Wed, 4 Dec 2024 11:32:08 +0100 Subject: [PATCH 04/14] Types for signaturemanifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 44 +++++++++++++++++++++++++++++++++++++------- src/oci_types.go | 10 ++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 src/oci_types.go diff --git a/src/main.go b/src/main.go index d22f327..7c26f51 100644 --- a/src/main.go +++ b/src/main.go @@ -117,6 +117,25 @@ func getManifest(repo *remote.Repository, ctx context.Context, ref string) (map[ return manifest, nil } +func getBytes(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() + + manifestContent, err := io.ReadAll(manifestStream) + if err != nil { + return nil, err + } + + return manifestContent, nil +} + func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag string, cname string) (string, error) { index, err := getManifest(repo, ctx, tag) if err != nil { @@ -294,23 +313,35 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e return nil } +// TODO exit codes and error handling, replace panics with fmt.Println to stderr + os.Exit func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) { signatureTag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" - signatureManifest, err := getManifest(repo, ctx, signatureTag) + // TODO golang type for Manifest? + signatureManifestBytes, err := getBytes(repo, ctx, signatureTag) if err != nil { panic(err) } - signatureStr := signatureManifest["layers"].([]interface{})[0].(map[string]interface{})["annotations"].(map[string]interface{})["dev.cosignproject.cosign/signature"].(string) + + signatureManifest := SignatureManifest{} + err = json.Unmarshal(signatureManifestBytes, &signatureManifest) + if err != nil { + panic(err) + } + // types + signatureStr := signatureManifest.Layers[0].Annotations.Signature signature, err := base64.StdEncoding.DecodeString(signatureStr) - messageHashStr := strings.Trim(signatureManifest["layers"].([]interface{})[0].(map[string]interface{})["digest"].(string), "sha256:") + // TODO proper verification + // 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. + // types + messageHashStr := strings.Trim(signatureManifest.Layers[0].Digest, "sha256:") messageHash, err := hex.DecodeString(messageHashStr) if err != nil { - fmt.Printf("Error decoding messageHashStr: %s\n", err) + panic(fmt.Errorf("Error decoding messageHashStr: %s\n", err)) } - fmt.Println(signature) - fmt.Println(messageHash) + // TODO key input (two methods, one baked in key, and command line flag) // Key -> hardcode / read from disk keyData, err := os.ReadFile("key") if err != nil { @@ -338,7 +369,6 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) } panic("bye") - } const ERR_INVALID_ARGUMENTS = 1 diff --git a/src/oci_types.go b/src/oci_types.go new file mode 100644 index 0000000..96db392 --- /dev/null +++ b/src/oci_types.go @@ -0,0 +1,10 @@ +package main + +type SignatureManifest struct { + Layers []struct { + Digest string `json:"digest"` + Annotations struct { + Signature string `json:"dev.cosignproject.cosign/signature"` + } `json:"annotations"` + } `json:"layers"` +} From d1bfabdb632b57e687c71274a91513a24eb7aeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Wed, 4 Dec 2024 11:49:10 +0100 Subject: [PATCH 05/14] New error codes and error messages for signature verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/main.go b/src/main.go index 7c26f51..3f662a7 100644 --- a/src/main.go +++ b/src/main.go @@ -313,23 +313,27 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e return nil } -// TODO exit codes and error handling, replace panics with fmt.Println to stderr + os.Exit func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) { signatureTag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" - // TODO golang type for Manifest? signatureManifestBytes, err := getBytes(repo, ctx, signatureTag) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_NETWORK_PROBLEMS) } signatureManifest := SignatureManifest{} err = json.Unmarshal(signatureManifestBytes, &signatureManifest) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_INVALID_FORMAT) } // 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_INVALID_FORMAT) + } // TODO proper verification // Here we pull the messageHashStr. This is insufficient for a proper signature verification. We have to @@ -338,42 +342,49 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) messageHashStr := strings.Trim(signatureManifest.Layers[0].Digest, "sha256:") messageHash, err := hex.DecodeString(messageHashStr) if err != nil { - panic(fmt.Errorf("Error decoding messageHashStr: %s\n", err)) + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(ERR_INVALID_FORMAT) } // TODO key input (two methods, one baked in key, and command line flag) // Key -> hardcode / read from disk keyData, err := os.ReadFile("key") if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, "Error loading key:", err) + os.Exit(ERR_SYSTEM_FAILURE) } - fmt.Println("Keyfile loaded.") block, _ := pem.Decode(keyData) if block == nil { - panic("unable to PEM decode provided key") + fmt.Fprintln(os.Stderr, "Error decoding pemdata.") + os.Exit(ERR_INVALID_FORMAT) } - fmt.Println("PEM data loaded.") pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, "Error parsing key:", err) + os.Exit(ERR_INVALID_FORMAT) } - fmt.Println("Key loaded.") err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, messageHash[:], signature) if err == nil { fmt.Println("Verified OK") } else { - panic(err) + fmt.Fprintln(os.Stderr, "Invalid signature:", err) + os.Exit(ERR_INVALID_SIGNATURE) } panic("bye") } -const ERR_INVALID_ARGUMENTS = 1 -const ERR_SYSTEM_FAILURE = 2 -const ERR_NETWORK_PROBLEMS = 3 +const ( + _ = iota + ERR_INVALID_ARGUMENTS + ERR_SYSTEM_FAILURE + ERR_NETWORK_PROBLEMS + ERR_INVALID_FORMAT // errors during type conversion + ERR_INVALID_SIGNATURE // manifest not signed correctly +) func main() { flag_set := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) From 322b88a498333e11f26e036652295ef4b6d4b507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Wed, 4 Dec 2024 12:33:03 +0100 Subject: [PATCH 06/14] Local verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 68 +++++++++++++++++++++++++++++++++++++++++------- src/oci_types.go | 12 +++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/main.go b/src/main.go index 3f662a7..46b95c6 100644 --- a/src/main.go +++ b/src/main.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "context" "crypto" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" @@ -117,7 +119,7 @@ func getManifest(repo *remote.Repository, ctx context.Context, ref string) (map[ return manifest, nil } -func getBytes(repo *remote.Repository, ctx context.Context, ref string) ([]byte, error) { +func getManifestBytes(repo *remote.Repository, ctx context.Context, ref string) ([]byte, error) { manifestDescriptor, err := repo.Resolve(ctx, ref) if err != nil { return nil, err @@ -136,6 +138,25 @@ func getBytes(repo *remote.Repository, ctx context.Context, ref string) ([]byte, return manifestContent, nil } +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() + + blobContent, err := io.ReadAll(manifestStream) + if err != nil { + return nil, err + } + + return blobContent, nil +} + func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag string, cname string) (string, error) { index, err := getManifest(repo, ctx, tag) if err != nil { @@ -315,7 +336,7 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) { signatureTag := strings.Replace(digest, "sha256:", "sha256-", 1) + ".sig" - signatureManifestBytes, err := getBytes(repo, ctx, signatureTag) + signatureManifestBytes, err := getManifestBytes(repo, ctx, signatureTag) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(ERR_NETWORK_PROBLEMS) @@ -335,16 +356,43 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) os.Exit(ERR_INVALID_FORMAT) } - // TODO proper verification + 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_INVALID_FORMAT) + } + // 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. - // types - messageHashStr := strings.Trim(signatureManifest.Layers[0].Digest, "sha256:") - messageHash, err := hex.DecodeString(messageHashStr) + + // 1. Get signed message + message, err := getBlobBytes(repo, ctx, messageHashStr) if err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) + 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_INVALID_FORMAT) } + // 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_INVALID_SIGNATURE) + } + + // 3. hash the signature message + hashed := sha256.Sum256(message) + // 4. check if hash in signaturemanifest == locally computed hash of the message + if !bytes.Equal(hashed[:], 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_INVALID_SIGNATURE) + } // TODO key input (two methods, one baked in key, and command line flag) // Key -> hardcode / read from disk @@ -366,7 +414,7 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) os.Exit(ERR_INVALID_FORMAT) } - err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, messageHash[:], signature) + err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, messageHashFromManifest[:], signature) if err == nil { fmt.Println("Verified OK") } else { @@ -374,7 +422,6 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) os.Exit(ERR_INVALID_SIGNATURE) } - panic("bye") } const ( @@ -449,10 +496,11 @@ func main() { os.Exit(ERR_NETWORK_PROBLEMS) } if layer == "" || size == 0 { - fmt.Println(os.Stderr, "No layer found for "+cname+" version: "+version+" and mediatype"+*media_type+" on "+*repo_url) + fmt.Fprintln(os.Stderr, "No layer found for "+cname+" version: "+version+" and mediatype"+*media_type+" on "+*repo_url) os.Exit(ERR_SYSTEM_FAILURE) } + panic(layer) space_required := size + (1024 * 1024) target_path := *target_dir + "/" + cname + "-" + version + "+3.efi" diff --git a/src/oci_types.go b/src/oci_types.go index 96db392..bc48664 100644 --- a/src/oci_types.go +++ b/src/oci_types.go @@ -8,3 +8,15 @@ type SignatureManifest struct { } `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"` +} From 2ae6c7be13b7f1cb9423f9f25c8482f1c368fe97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Wed, 4 Dec 2024 13:06:14 +0100 Subject: [PATCH 07/14] Add key param and internal key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 45 +++++++++++++++++++++++++---------------- src/verification_key.go | 17 ++++++++++++++++ 2 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 src/verification_key.go diff --git a/src/main.go b/src/main.go index 46b95c6..e9c0259 100644 --- a/src/main.go +++ b/src/main.go @@ -334,7 +334,7 @@ func garbageClean(directory, cname, current_version string, size_wanted int64) e return nil } -func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) { +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 { @@ -394,14 +394,32 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) os.Exit(ERR_INVALID_SIGNATURE) } - // TODO key input (two methods, one baked in key, and command line flag) - // Key -> hardcode / read from disk - keyData, err := os.ReadFile("key") - if err != nil { - fmt.Fprintln(os.Stderr, "Error loading key:", err) - os.Exit(ERR_SYSTEM_FAILURE) + pubKey := getVerificationKey(verificationKeyFile) + + err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, messageHashFromManifest[:], signature) + if err == nil { + fmt.Println("Verified OK") + } else { + fmt.Fprintln(os.Stderr, "Invalid signature:", err) + os.Exit(ERR_INVALID_SIGNATURE) } +} + +func getVerificationKey(verificationKeyFile string) *rsa.PublicKey { + if verificationKeyFile == "" { + return pubKeyFromBytes(defaultKey) + } else { + keyData, err := os.ReadFile(verificationKeyFile) + if err != nil { + fmt.Fprintln(os.Stderr, "Error loading key:", err) + os.Exit(ERR_SYSTEM_FAILURE) + } + return pubKeyFromBytes(keyData) + } +} + +func pubKeyFromBytes(keyData []byte) *rsa.PublicKey { block, _ := pem.Decode(keyData) if block == nil { fmt.Fprintln(os.Stderr, "Error decoding pemdata.") @@ -413,15 +431,7 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest string) fmt.Fprintln(os.Stderr, "Error parsing key:", err) os.Exit(ERR_INVALID_FORMAT) } - - err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, messageHashFromManifest[:], signature) - if err == nil { - fmt.Println("Verified OK") - } else { - fmt.Fprintln(os.Stderr, "Invalid signature:", err) - os.Exit(ERR_INVALID_SIGNATURE) - } - + return pubKey.(*rsa.PublicKey) } const ( @@ -442,6 +452,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", "", "path to verification key file") flag_set.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) @@ -488,7 +499,7 @@ func main() { } // verify the signature here - verifyManifest(repo, ctx, digest) + verifyManifest(repo, ctx, digest, *verification_key_file) layer, size, err := getLayerByMediaType(repo, ctx, digest, *media_type) if err != nil { diff --git a/src/verification_key.go b/src/verification_key.go new file mode 100644 index 0000000..6ccb8c8 --- /dev/null +++ b/src/verification_key.go @@ -0,0 +1,17 @@ +package main + +var defaultKey = []byte(` +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAro2/c3xu4eD25Lw/oJ6P +mqQFVXO5rsh0zZWdRyTegCUVDEPGrEpb1p2X1I0/xq2AmS/rmSQcfpoALsgDlwzv +0Ici+HQfQPujTOiYX5CpqhBm1dDFNPNY55Mor42N6s1/QEOlms3SP2AknMUdtBC5 +bnCNsPezOVYaDLDrJxW9/IiOXnehm/6fFOekkpnErq5NqghPUzT0V+DliJwU3yYI +z/OaPv+PeqjIogj/iWs7ttbQ1twM2kvd/rLKD7R3UcaljWcpfvsENr57YeG8jQkg +VuqX5RnMs/vCxNw/V9uhr8YV04GsurcjaRZXvMf2Ls2Ly473O6mSDXHW92QNCE5i +7Y+EffQG8RTn4/qZPt7gUMOhWzj7RjukcYkc51QJSNveCkSwzWsjerRC451J0TDb +nZIONOkMh6bIn/7zO8JL0DyK1n26IiWqj140zwMcmjji4+NzwxG8yIY/0mKVMFql +g4UeEsPlQPT4GaQm+goiNGjQO9UkHYbN6yfkFw/8wpAdhbHm+IcnNfmPeLen32Az +ZkFKrphwvMFOG07hMKSaiOQE/6ls/Ximr0spa+PVsprWdoYb2RkxCWaZfrCdNZw9 +BVyXt076CkhrqfNboNKS3ji41DO4cAkrmnQKBESS01HEbrlJYitqP0vWSbSpO+7H +zLy1Za8aXR8tBAnjr7g9Pm0CAwEAAQ== +-----END PUBLIC KEY-----`) From 1819ae945b41c12f318a9330399feaad5405c872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Wed, 4 Dec 2024 13:09:42 +0100 Subject: [PATCH 08/14] Remove debugging stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.go b/src/main.go index e9c0259..39fbcb7 100644 --- a/src/main.go +++ b/src/main.go @@ -511,7 +511,6 @@ func main() { os.Exit(ERR_SYSTEM_FAILURE) } - panic(layer) space_required := size + (1024 * 1024) target_path := *target_dir + "/" + cname + "-" + version + "+3.efi" From 3f84f486ca41a8e529abf1ccbf24c11dd5b580c7 Mon Sep 17 00:00:00 2001 From: Paul Hildebrandt Date: Wed, 4 Dec 2024 15:21:17 +0100 Subject: [PATCH 09/14] better types for getManifestDigestByCname Signed-off-by: Paul Hildebrandt --- src/main.go | 28 +++++++++++-------------- src/oci_types.go | 22 -------------------- src/types.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 38 deletions(-) delete mode 100644 src/oci_types.go create mode 100644 src/types.go diff --git a/src/main.go b/src/main.go index 39fbcb7..0977ecf 100644 --- a/src/main.go +++ b/src/main.go @@ -158,30 +158,26 @@ func getBlobBytes(repo *remote.Repository, ctx context.Context, ref string) ([]b } func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag string, cname string) (string, error) { - index, 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 index["manifests"].([]interface{}) { - item := entry.(map[string]interface{}) - item_digest := item["digest"].(string) - // annotations only exist for gardenlinux artifacts, "normal" container runtime entries do not have that field - if item["annotations"] == nil { - continue - } - 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) { diff --git a/src/oci_types.go b/src/oci_types.go deleted file mode 100644 index bc48664..0000000 --- a/src/oci_types.go +++ /dev/null @@ -1,22 +0,0 @@ -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"` -} diff --git a/src/types.go b/src/types.go new file mode 100644 index 0000000..7cbded6 --- /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 int `json:"size"` + } `json:"layers"` +} From 9e0576abd617e25fa61ea27bd3309ce5eb509344 Mon Sep 17 00:00:00 2001 From: Paul Hildebrandt Date: Wed, 4 Dec 2024 16:02:42 +0100 Subject: [PATCH 10/14] better types for everyone Signed-off-by: Paul Hildebrandt --- src/main.go | 55 ++++++++++++---------------------------------------- src/types.go | 2 +- 2 files changed, 13 insertions(+), 44 deletions(-) diff --git a/src/main.go b/src/main.go index 0977ecf..cfc47fb 100644 --- a/src/main.go +++ b/src/main.go @@ -92,33 +92,6 @@ 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) - if err != nil { - return nil, err - } - - mainfest_stream, err := repo.Fetch(ctx, manifest_descriptor) - if err != nil { - return nil, err - } - defer mainfest_stream.Close() - - var manifest map[string]interface{} - - manifest_content, err := io.ReadAll(mainfest_stream) - if err != nil { - return nil, err - } - - err = json.Unmarshal(manifest_content, &manifest) - if err != nil { - return nil, err - } - - return manifest, nil -} - func getManifestBytes(repo *remote.Repository, ctx context.Context, ref string) ([]byte, error) { manifestDescriptor, err := repo.Resolve(ctx, ref) if err != nil { @@ -181,28 +154,24 @@ func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag } 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) { @@ -513,7 +482,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) } @@ -521,7 +490,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 index 7cbded6..d53905d 100644 --- a/src/types.go +++ b/src/types.go @@ -48,6 +48,6 @@ type Manifest struct { Layers []struct { MediaType string `json:"mediaType"` Digest string `json:"digest"` - Size int `json:"size"` + Size uint64 `json:"size"` } `json:"layers"` } From ac8885eb2e4b7796a2a67ccc152a310b1e33984f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Thu, 5 Dec 2024 15:20:17 +0100 Subject: [PATCH 11/14] consolidate error codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main.go b/src/main.go index cfc47fb..3a1752a 100644 --- a/src/main.go +++ b/src/main.go @@ -311,21 +311,21 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi err = json.Unmarshal(signatureManifestBytes, &signatureManifest) if err != nil { fmt.Fprintln(os.Stderr, "Error:", err) - os.Exit(ERR_INVALID_FORMAT) + 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_INVALID_FORMAT) + 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_INVALID_FORMAT) + os.Exit(ERR_SYSTEM_FAILURE) } // Here we pull the messageHashStr. This is insufficient for a proper signature verification. We have to @@ -341,12 +341,12 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi err = json.Unmarshal(message, &signatureMessage) if err != nil { fmt.Fprintln(os.Stderr, "Error unmarshalling signature message:", err) - os.Exit(ERR_INVALID_FORMAT) + 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_INVALID_SIGNATURE) + os.Exit(ERR_SYSTEM_FAILURE) } // 3. hash the signature message @@ -356,7 +356,7 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi 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_INVALID_SIGNATURE) + os.Exit(ERR_SYSTEM_FAILURE) } pubKey := getVerificationKey(verificationKeyFile) @@ -366,7 +366,7 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi fmt.Println("Verified OK") } else { fmt.Fprintln(os.Stderr, "Invalid signature:", err) - os.Exit(ERR_INVALID_SIGNATURE) + os.Exit(ERR_SYSTEM_FAILURE) } } @@ -388,24 +388,23 @@ func pubKeyFromBytes(keyData []byte) *rsa.PublicKey { block, _ := pem.Decode(keyData) if block == nil { fmt.Fprintln(os.Stderr, "Error decoding pemdata.") - os.Exit(ERR_INVALID_FORMAT) + 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_INVALID_FORMAT) + 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 - ERR_SYSTEM_FAILURE - ERR_NETWORK_PROBLEMS - ERR_INVALID_FORMAT // errors during type conversion - ERR_INVALID_SIGNATURE // manifest not signed correctly + _ = iota + ERR_INVALID_ARGUMENTS // permanent + ERR_SYSTEM_FAILURE // permanent + ERR_NETWORK_PROBLEMS // retry makes sense ) func main() { From 32e72afe53e52721b093bb97608f9d38266a60ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Thu, 5 Dec 2024 15:24:57 +0100 Subject: [PATCH 12/14] Remove hardcoded key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 18 +++++------------- src/verification_key.go | 17 ----------------- 2 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 src/verification_key.go diff --git a/src/main.go b/src/main.go index 3a1752a..de6bcc3 100644 --- a/src/main.go +++ b/src/main.go @@ -372,19 +372,11 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi } func getVerificationKey(verificationKeyFile string) *rsa.PublicKey { - if verificationKeyFile == "" { - return pubKeyFromBytes(defaultKey) - } else { - keyData, err := os.ReadFile(verificationKeyFile) - if err != nil { - fmt.Fprintln(os.Stderr, "Error loading key:", err) - os.Exit(ERR_SYSTEM_FAILURE) - } - return pubKeyFromBytes(keyData) + keyData, err := os.ReadFile(verificationKeyFile) + if err != nil { + fmt.Fprintln(os.Stderr, "Error loading key:", err) + os.Exit(ERR_SYSTEM_FAILURE) } -} - -func pubKeyFromBytes(keyData []byte) *rsa.PublicKey { block, _ := pem.Decode(keyData) if block == nil { fmt.Fprintln(os.Stderr, "Error decoding pemdata.") @@ -416,7 +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", "", "path to verification key file") + verification_key_file := flag_set.String("verification-key", "/etc/gardenlinux/signing_key.pem", "path to verification key file") flag_set.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) diff --git a/src/verification_key.go b/src/verification_key.go deleted file mode 100644 index 6ccb8c8..0000000 --- a/src/verification_key.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -var defaultKey = []byte(` ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAro2/c3xu4eD25Lw/oJ6P -mqQFVXO5rsh0zZWdRyTegCUVDEPGrEpb1p2X1I0/xq2AmS/rmSQcfpoALsgDlwzv -0Ici+HQfQPujTOiYX5CpqhBm1dDFNPNY55Mor42N6s1/QEOlms3SP2AknMUdtBC5 -bnCNsPezOVYaDLDrJxW9/IiOXnehm/6fFOekkpnErq5NqghPUzT0V+DliJwU3yYI -z/OaPv+PeqjIogj/iWs7ttbQ1twM2kvd/rLKD7R3UcaljWcpfvsENr57YeG8jQkg -VuqX5RnMs/vCxNw/V9uhr8YV04GsurcjaRZXvMf2Ls2Ly473O6mSDXHW92QNCE5i -7Y+EffQG8RTn4/qZPt7gUMOhWzj7RjukcYkc51QJSNveCkSwzWsjerRC451J0TDb -nZIONOkMh6bIn/7zO8JL0DyK1n26IiWqj140zwMcmjji4+NzwxG8yIY/0mKVMFql -g4UeEsPlQPT4GaQm+goiNGjQO9UkHYbN6yfkFw/8wpAdhbHm+IcnNfmPeLen32Az -ZkFKrphwvMFOG07hMKSaiOQE/6ls/Ximr0spa+PVsprWdoYb2RkxCWaZfrCdNZw9 -BVyXt076CkhrqfNboNKS3ji41DO4cAkrmnQKBESS01HEbrlJYitqP0vWSbSpO+7H -zLy1Za8aXR8tBAnjr7g9Pm0CAwEAAQ== ------END PUBLIC KEY-----`) From 72eb8874149484a747c63475ec59b3d2d73e2b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20M=C3=BCnch?= Date: Thu, 5 Dec 2024 15:26:38 +0100 Subject: [PATCH 13/14] check signed message using locally computed hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Malte Münch --- src/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.go b/src/main.go index de6bcc3..ced251c 100644 --- a/src/main.go +++ b/src/main.go @@ -350,9 +350,9 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi } // 3. hash the signature message - hashed := sha256.Sum256(message) + local_hash := sha256.Sum256(message) // 4. check if hash in signaturemanifest == locally computed hash of the message - if !bytes.Equal(hashed[:], messageHashFromManifest) { + 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) @@ -361,7 +361,7 @@ func verifyManifest(repo *remote.Repository, ctx context.Context, digest, verifi pubKey := getVerificationKey(verificationKeyFile) - err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, messageHashFromManifest[:], signature) + err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, local_hash[:], signature) if err == nil { fmt.Println("Verified OK") } else { From 8fc5975273a3133d794610792749693e137508af Mon Sep 17 00:00:00 2001 From: nkraetzschmar <9020053+nkraetzschmar@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:35:13 +0100 Subject: [PATCH 14/14] s/signing_key.pem/oci_signing_key.pem/ --- src/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.go b/src/main.go index ced251c..54190cd 100644 --- a/src/main.go +++ b/src/main.go @@ -408,7 +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/signing_key.pem", "path to verification key file") + 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])