mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:24:18 -04:00
277 lines
9.7 KiB
Go
277 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/updatemanager/reposign"
|
|
)
|
|
|
|
const (
|
|
envArtifactPrivateKey = "NB_ARTIFACT_PRIV_KEY"
|
|
)
|
|
|
|
var (
|
|
signArtifactPrivKeyFile string
|
|
signArtifactArtifactFile string
|
|
|
|
verifyArtifactPubKeyFile string
|
|
verifyArtifactFile string
|
|
verifyArtifactSignatureFile string
|
|
|
|
verifyArtifactKeyPubKeyFile string
|
|
verifyArtifactKeyRootPubKeyFile string
|
|
verifyArtifactKeySignatureFile string
|
|
verifyArtifactKeyRevocationFile string
|
|
)
|
|
|
|
var signArtifactCmd = &cobra.Command{
|
|
Use: "sign-artifact",
|
|
Short: "Sign an artifact using an artifact private key",
|
|
Long: `Sign a software artifact (e.g., update bundle or binary) using the artifact's private key.
|
|
This command produces a detached signature that can be verified using the corresponding artifact public key.`,
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := handleSignArtifact(cmd, signArtifactPrivKeyFile, signArtifactArtifactFile); err != nil {
|
|
return fmt.Errorf("failed to sign artifact: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var verifyArtifactCmd = &cobra.Command{
|
|
Use: "verify-artifact",
|
|
Short: "Verify an artifact signature using an artifact public key",
|
|
Long: `Verify a software artifact signature using the artifact's public key.`,
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := handleVerifyArtifact(cmd, verifyArtifactPubKeyFile, verifyArtifactFile, verifyArtifactSignatureFile); err != nil {
|
|
return fmt.Errorf("failed to verify artifact: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var verifyArtifactKeyCmd = &cobra.Command{
|
|
Use: "verify-artifact-key",
|
|
Short: "Verify an artifact public key was signed by a root key",
|
|
Long: `Verify that an artifact public key (or bundle) was properly signed by a root key.
|
|
This validates the chain of trust from the root key to the artifact key.`,
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := handleVerifyArtifactKey(cmd, verifyArtifactKeyPubKeyFile, verifyArtifactKeyRootPubKeyFile, verifyArtifactKeySignatureFile, verifyArtifactKeyRevocationFile); err != nil {
|
|
return fmt.Errorf("failed to verify artifact key: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(signArtifactCmd)
|
|
rootCmd.AddCommand(verifyArtifactCmd)
|
|
rootCmd.AddCommand(verifyArtifactKeyCmd)
|
|
|
|
signArtifactCmd.Flags().StringVar(&signArtifactPrivKeyFile, "artifact-key-file", "", fmt.Sprintf("Path to the artifact private key file used for signing (or set %s env var)", envArtifactPrivateKey))
|
|
signArtifactCmd.Flags().StringVar(&signArtifactArtifactFile, "artifact-file", "", "Path to the artifact to be signed")
|
|
|
|
// artifact-file is required, but artifact-key-file can come from env var
|
|
if err := signArtifactCmd.MarkFlagRequired("artifact-file"); err != nil {
|
|
panic(fmt.Errorf("mark artifact-file as required: %w", err))
|
|
}
|
|
|
|
verifyArtifactCmd.Flags().StringVar(&verifyArtifactPubKeyFile, "artifact-public-key-file", "", "Path to the artifact public key file")
|
|
verifyArtifactCmd.Flags().StringVar(&verifyArtifactFile, "artifact-file", "", "Path to the artifact to be verified")
|
|
verifyArtifactCmd.Flags().StringVar(&verifyArtifactSignatureFile, "signature-file", "", "Path to the signature file")
|
|
|
|
if err := verifyArtifactCmd.MarkFlagRequired("artifact-public-key-file"); err != nil {
|
|
panic(fmt.Errorf("mark artifact-public-key-file as required: %w", err))
|
|
}
|
|
if err := verifyArtifactCmd.MarkFlagRequired("artifact-file"); err != nil {
|
|
panic(fmt.Errorf("mark artifact-file as required: %w", err))
|
|
}
|
|
if err := verifyArtifactCmd.MarkFlagRequired("signature-file"); err != nil {
|
|
panic(fmt.Errorf("mark signature-file as required: %w", err))
|
|
}
|
|
|
|
verifyArtifactKeyCmd.Flags().StringVar(&verifyArtifactKeyPubKeyFile, "artifact-key-file", "", "Path to the artifact public key file or bundle")
|
|
verifyArtifactKeyCmd.Flags().StringVar(&verifyArtifactKeyRootPubKeyFile, "root-key-file", "", "Path to the root public key file or bundle")
|
|
verifyArtifactKeyCmd.Flags().StringVar(&verifyArtifactKeySignatureFile, "signature-file", "", "Path to the signature file")
|
|
verifyArtifactKeyCmd.Flags().StringVar(&verifyArtifactKeyRevocationFile, "revocation-file", "", "Path to the revocation list file (optional)")
|
|
|
|
if err := verifyArtifactKeyCmd.MarkFlagRequired("artifact-key-file"); err != nil {
|
|
panic(fmt.Errorf("mark artifact-key-file as required: %w", err))
|
|
}
|
|
if err := verifyArtifactKeyCmd.MarkFlagRequired("root-key-file"); err != nil {
|
|
panic(fmt.Errorf("mark root-key-file as required: %w", err))
|
|
}
|
|
if err := verifyArtifactKeyCmd.MarkFlagRequired("signature-file"); err != nil {
|
|
panic(fmt.Errorf("mark signature-file as required: %w", err))
|
|
}
|
|
}
|
|
|
|
func handleSignArtifact(cmd *cobra.Command, privKeyFile, artifactFile string) error {
|
|
cmd.Println("🖋️ Signing artifact...")
|
|
|
|
// Load private key from env var or file
|
|
var privKeyPEM []byte
|
|
var err error
|
|
|
|
if envKey := os.Getenv(envArtifactPrivateKey); envKey != "" {
|
|
// Use key from environment variable
|
|
privKeyPEM = []byte(envKey)
|
|
} else if privKeyFile != "" {
|
|
// Fall back to file
|
|
privKeyPEM, err = os.ReadFile(privKeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read private key file: %w", err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("artifact private key must be provided via %s environment variable or --artifact-key-file flag", envArtifactPrivateKey)
|
|
}
|
|
|
|
privateKey, err := reposign.ParseArtifactKey(privKeyPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse artifact private key: %w", err)
|
|
}
|
|
|
|
artifactData, err := os.ReadFile(artifactFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read artifact file: %w", err)
|
|
}
|
|
|
|
signature, err := reposign.SignData(privateKey, artifactData)
|
|
if err != nil {
|
|
return fmt.Errorf("sign artifact: %w", err)
|
|
}
|
|
|
|
sigFile := artifactFile + ".sig"
|
|
if err := os.WriteFile(artifactFile+".sig", signature, 0o600); err != nil {
|
|
return fmt.Errorf("write signature file (%s): %w", sigFile, err)
|
|
}
|
|
|
|
cmd.Printf("✅ Artifact signed successfully.\n")
|
|
cmd.Printf("Signature file: %s\n", sigFile)
|
|
return nil
|
|
}
|
|
|
|
func handleVerifyArtifact(cmd *cobra.Command, pubKeyFile, artifactFile, signatureFile string) error {
|
|
cmd.Println("🔍 Verifying artifact...")
|
|
|
|
// Read artifact public key
|
|
pubKeyPEM, err := os.ReadFile(pubKeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read public key file: %w", err)
|
|
}
|
|
|
|
publicKey, err := reposign.ParseArtifactPubKey(pubKeyPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse artifact public key: %w", err)
|
|
}
|
|
|
|
// Read artifact data
|
|
artifactData, err := os.ReadFile(artifactFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read artifact file: %w", err)
|
|
}
|
|
|
|
// Read signature
|
|
sigBytes, err := os.ReadFile(signatureFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read signature file: %w", err)
|
|
}
|
|
|
|
signature, err := reposign.ParseSignature(sigBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse signature: %w", err)
|
|
}
|
|
|
|
// Validate artifact
|
|
if err := reposign.ValidateArtifact([]reposign.PublicKey{publicKey}, artifactData, *signature); err != nil {
|
|
return fmt.Errorf("artifact verification failed: %w", err)
|
|
}
|
|
|
|
cmd.Println("✅ Artifact signature is valid")
|
|
cmd.Printf("Artifact: %s\n", artifactFile)
|
|
cmd.Printf("Signed by key: %s\n", signature.KeyID)
|
|
cmd.Printf("Signature timestamp: %s\n", signature.Timestamp.Format("2006-01-02 15:04:05 MST"))
|
|
return nil
|
|
}
|
|
|
|
func handleVerifyArtifactKey(cmd *cobra.Command, artifactKeyFile, rootKeyFile, signatureFile, revocationFile string) error {
|
|
cmd.Println("🔍 Verifying artifact key...")
|
|
|
|
// Read artifact key data
|
|
artifactKeyData, err := os.ReadFile(artifactKeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read artifact key file: %w", err)
|
|
}
|
|
|
|
// Read root public key(s)
|
|
rootKeyData, err := os.ReadFile(rootKeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read root key file: %w", err)
|
|
}
|
|
|
|
rootPublicKeys, err := parseRootPublicKeys(rootKeyData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse root public key(s): %w", err)
|
|
}
|
|
|
|
// Read signature
|
|
sigBytes, err := os.ReadFile(signatureFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read signature file: %w", err)
|
|
}
|
|
|
|
signature, err := reposign.ParseSignature(sigBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse signature: %w", err)
|
|
}
|
|
|
|
// Read optional revocation list
|
|
var revocationList *reposign.RevocationList
|
|
if revocationFile != "" {
|
|
revData, err := os.ReadFile(revocationFile)
|
|
if err != nil {
|
|
return fmt.Errorf("read revocation file: %w", err)
|
|
}
|
|
|
|
revocationList, err = reposign.ParseRevocationList(revData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse revocation list: %w", err)
|
|
}
|
|
}
|
|
|
|
// Validate artifact key(s)
|
|
validKeys, err := reposign.ValidateArtifactKeys(rootPublicKeys, artifactKeyData, *signature, revocationList)
|
|
if err != nil {
|
|
return fmt.Errorf("artifact key verification failed: %w", err)
|
|
}
|
|
|
|
cmd.Println("✅ Artifact key(s) verified successfully")
|
|
cmd.Printf("Signed by root key: %s\n", signature.KeyID)
|
|
cmd.Printf("Signature timestamp: %s\n", signature.Timestamp.Format("2006-01-02 15:04:05 MST"))
|
|
cmd.Printf("\nValid artifact keys (%d):\n", len(validKeys))
|
|
for i, key := range validKeys {
|
|
cmd.Printf(" [%d] Key ID: %s\n", i+1, key.Metadata.ID)
|
|
cmd.Printf(" Created: %s\n", key.Metadata.CreatedAt.Format("2006-01-02 15:04:05 MST"))
|
|
if !key.Metadata.ExpiresAt.IsZero() {
|
|
cmd.Printf(" Expires: %s\n", key.Metadata.ExpiresAt.Format("2006-01-02 15:04:05 MST"))
|
|
} else {
|
|
cmd.Printf(" Expires: Never\n")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseRootPublicKeys parses a root public key from PEM data
|
|
func parseRootPublicKeys(data []byte) ([]reposign.PublicKey, error) {
|
|
key, err := reposign.ParseRootPublicKey(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []reposign.PublicKey{key}, nil
|
|
}
|