588fed2aa9
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>
104 lines
3.7 KiB
Go
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
|
|
}
|