package signedjobs import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "encoding/pem" "testing" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/authz" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" "golang.org/x/crypto/ssh" ) // In-test SSHSIG minter (mirrors internal/reconcile/mint_test.go's framing) so the runner tests // exercise the REAL authz.Verifier + reconcile.Gate over genuinely-signed blobs — a positive case // verifying proves the framing, and the adversarial cases (non-pinned/replay/expired/retarget/ // forged) exercise the real rejection path end-to-end. Production authz stays verify-only. const sshsigMagic = "SSHSIG" type sshsigBlob struct { Version uint32 PublicKey string Namespace string Reserved string HashAlgo string Signature string } func signedDataForTest(ns string, msg []byte) []byte { h := sha256Sum(msg) body := ssh.Marshal(struct { Namespace string Reserved string HashAlgo string Hash []byte }{ns, "", "sha256", h}) return append([]byte(sshsigMagic), body...) } func mintArmor(pubMarshaled []byte, namespace string, message []byte, sign func([]byte) ssh.Signature) []byte { sb := &sshsigBlob{Version: 1, PublicKey: string(pubMarshaled), Namespace: namespace, Reserved: "", HashAlgo: "sha256"} sig := sign(signedDataForTest(namespace, message)) sb.Signature = string(ssh.Marshal(&sig)) raw := append([]byte(sshsigMagic), ssh.Marshal(sb)...) return pem.EncodeToMemory(&pem.Block{Type: "SSH SIGNATURE", Bytes: raw}) } type testSigner struct { pub ssh.PublicKey line string sign func([]byte) ssh.Signature } func newTestSigner(t *testing.T) testSigner { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } sshPub, err := ssh.NewPublicKey(pub) if err != nil { t.Fatal(err) } return testSigner{ pub: sshPub, line: string(ssh.MarshalAuthorizedKey(sshPub)), sign: func(d []byte) ssh.Signature { return ssh.Signature{Format: ssh.KeyAlgoED25519, Blob: ed25519.Sign(priv, d)} }, } } func (s testSigner) allowed(t *testing.T, keyID string, role authz.KeyRole) authz.AllowedSigner { t.Helper() as, err := authz.NewAllowedSigner(keyID, role, s.line) if err != nil { t.Fatalf("NewAllowedSigner: %v", err) } return as } // mintJob builds a hub.JobWire carrying a signed storage_wipe envelope from the given signer. func mintJob(t *testing.T, s testSigner, jobID, host, guest, keyID, paramsJSON string, issued, expires time.Time) hub.JobWire { return mintJobOp(t, s, "storage_wipe", jobID, host, guest, keyID, paramsJSON, issued, expires) } // mintJobOp is mintJob with an explicit op class (for non-wipe ops, e.g. agent_update ride-along). func mintJobOp(t *testing.T, s testSigner, op, jobID, host, guest, keyID, paramsJSON string, issued, expires time.Time) hub.JobWire { t.Helper() blob, err := authz.CanonicalBlob(op, host, guest, keyID, randNonce(), paramsJSON, issued, expires) if err != nil { t.Fatalf("CanonicalBlob: %v", err) } sig := mintArmor(s.pub.Marshal(), authz.Namespace, blob, s.sign) env := Envelope{OpBlobB64: base64.StdEncoding.EncodeToString(blob), SigArmored: string(sig)} envJSON, _ := json.Marshal(env) return hub.JobWire{JobID: jobID, BlobB64: base64.StdEncoding.EncodeToString(envJSON)} } func randNonce() string { var b [16]byte rand.Read(b[:]) const hexd = "0123456789abcdef" out := make([]byte, 32) for i, x := range b { out[i*2] = hexd[x>>4] out[i*2+1] = hexd[x&0x0f] } return string(out) } func sha256Sum(b []byte) []byte { h := sha256.Sum256(b) return h[:] }