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 }