Files
felhom-agent/internal/authz/mint_test.go
T
admin 588fed2aa9 slice 10B: operator-signed destructive completion (offline key + signing CLI) (v0.16.0)
A destructive op runs ONLY on a pinned-key-verified, nonce-fresh, in-window,
host-bound, durable-id-bound operator signature. New cmd/felhom-opsign signs
canonical OpBlobs offline via ssh-keygen -Y sign (hardware-ready); the signing
key is never in the hub or agent. New internal/signedjobs runner verifies each
queued blob through the gate and only on all-pass runs the WipeExecutor, which
re-resolves the DURABLE device id + re-inspects (8C) before mkfs — closing the
8C data-bearing-wipe pending_signature gap. New storage durable-device
resolution; authz.CanonicalBlob promoted to production. Real-crypto tests assert
valid executes and forged/replay/expired/retarget/non-pinned are rejected
(executor never called).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:14:16 +02:00

104 lines
3.7 KiB
Go

package authz
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/pem"
"testing"
"time"
"golang.org/x/crypto/ssh"
)
// Test helpers that MINT armored SSHSIGs in-Go (hermetic) — the inverse of the
// production framing. They reuse the production signedData()/sshsigBlob so a test
// can never drift from the verifier's notion of the signed bytes.
// canonicalBlob delegates to the production CanonicalBlob (so the in-Go test minting can never
// drift from the real signed-bytes path). Panics on a params error — tests pass valid JSON.
func canonicalBlob(op, hostID, guestID, keyID, nonce, paramsJSON string, issued, expires time.Time) []byte {
b, err := CanonicalBlob(op, hostID, guestID, keyID, nonce, paramsJSON, issued, expires)
if err != nil {
panic(err)
}
return b
}
// mintArmor builds an armored SSHSIG over message, using sign to produce the inner
// ssh.Signature over the recomputed SSHSIG signed-data.
func mintArmor(t *testing.T, pubMarshaled []byte, namespace, hashName string, message []byte, sign func([]byte) ssh.Signature) []byte {
t.Helper()
sb := &sshsigBlob{Version: 1, PublicKey: string(pubMarshaled), Namespace: namespace, Reserved: "", HashAlgo: hashName}
signed, err := signedData(sb, message)
if err != nil {
t.Fatalf("signedData: %v", err)
}
sig := sign(signed)
sb.Signature = string(ssh.Marshal(&sig))
raw := append([]byte(sshsigMagic), ssh.Marshal(sb)...)
return pem.EncodeToMemory(&pem.Block{Type: "SSH SIGNATURE", Bytes: raw})
}
// newEd25519Signer returns an ssh.PublicKey + a sign closure for a fresh ed25519 key.
func newEd25519Signer(t *testing.T) (ssh.PublicKey, func([]byte) ssh.Signature) {
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)
}
sign := func(signed []byte) ssh.Signature {
return ssh.Signature{Format: ssh.KeyAlgoED25519, Blob: ed25519.Sign(priv, signed)}
}
return sshPub, sign
}
// newSyntheticSKSigner emulates a FIDO2 sk-ssh-ed25519@openssh.com key with NO
// hardware (Phase 4 §5). It builds a spec-faithful sk public key and an sk-format
// signature: ed25519 over sha256(application)‖flags‖counter‖sha256(signed_data),
// sig.Blob = the raw ed25519 signature, sig.Rest = flags‖counter. It must verify
// through the UNCHANGED Verify path.
func newSyntheticSKSigner(t *testing.T) (ssh.PublicKey, func([]byte) ssh.Signature) {
t.Helper()
edPub, edPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
const application = "ssh:"
skBlob := ssh.Marshal(struct {
Name string
KeyBytes []byte
Application string
}{"sk-ssh-ed25519@openssh.com", []byte(edPub), application})
skPub, err := ssh.ParsePublicKey(skBlob)
if err != nil {
t.Fatalf("parse synthetic sk pubkey: %v", err)
}
if skPub.Type() != "sk-ssh-ed25519@openssh.com" {
t.Fatalf("sk pubkey type = %q", skPub.Type())
}
sign := func(signed []byte) ssh.Signature {
const flagUserPresence = byte(0x01) // required, else Verify rejects
const counter = uint32(1)
appDigest := sha256.Sum256([]byte(application))
dataDigest := sha256.Sum256(signed)
// original = appDigest ‖ flags ‖ counter(BE) ‖ dataDigest (x/crypto layout)
var original []byte
original = append(original, appDigest[:]...)
original = append(original, flagUserPresence)
original = binary.BigEndian.AppendUint32(original, counter)
original = append(original, dataDigest[:]...)
edSig := ed25519.Sign(edPriv, original)
// sig.Rest = skFields{Flags, Counter} = flags ‖ counter(BE)
rest := append([]byte{flagUserPresence}, binary.BigEndian.AppendUint32(nil, counter)...)
return ssh.Signature{Format: "sk-ssh-ed25519@openssh.com", Blob: edSig, Rest: rest}
}
return skPub, sign
}