package authz import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/binary" "encoding/pem" "fmt" "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 builds an op blob in the §2 canonical field order. (Self-consistent // for the in-Go path: we sign exactly these bytes and verify the same bytes. The // committed ssh-keygen fixture exercises real OpenSSH canonical interop.) func canonicalBlob(op, hostID, guestID, keyID, nonce, paramsJSON string, issued, expires time.Time) []byte { if paramsJSON == "" { paramsJSON = "{}" } return []byte(fmt.Sprintf( `{"expires_at":%q,"issued_at":%q,"key_id":%q,"nonce":%q,"op":%q,"params":%s,"target":{"guest_id":%q,"host_id":%q}}`, expires.UTC().Format(time.RFC3339), issued.UTC().Format(time.RFC3339), keyID, nonce, op, paramsJSON, guestID, hostID)) } // 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 }