package authz import ( "crypto/sha256" "crypto/sha512" "encoding/pem" "fmt" "hash" "golang.org/x/crypto/ssh" ) // SSHSIG framing — ported verbatim-in-spirit from phase4-signing-findings.md §7. // The only manual work is SSHSIG *framing*; all crypto and key-type dispatch is // x/crypto/ssh's (pub.Verify dispatches on the key's own algorithm, which is what // makes the verifier key-type-agnostic — ed25519 / sk-ssh-ed25519 / rsa / ecdsa). // No hand-rolled crypto. const sshsigMagic = "SSHSIG" // sshsigBlob is the binary SSHSIG body (after the 6-byte magic). Field order is // the SSH wire order — do not reorder. type sshsigBlob struct { Version uint32 PublicKey string Namespace string Reserved string HashAlgo string Signature string } func hashByName(n string) (hash.Hash, error) { switch n { case "sha256": return sha256.New(), nil case "sha512": return sha512.New(), nil } return nil, fmt.Errorf("%w: unsupported SSHSIG hash %q", ErrMalformed, n) } // parseArmoredSSHSIG decodes the `-----BEGIN SSH SIGNATURE-----` armor into the // SSHSIG body: pem.Decode → strip the literal 6-byte magic (not length-prefixed) // → ssh.Unmarshal. func parseArmoredSSHSIG(armored []byte) (*sshsigBlob, error) { block, _ := pem.Decode(armored) if block == nil || block.Type != "SSH SIGNATURE" { return nil, fmt.Errorf("%w: not an SSH SIGNATURE armor", ErrMalformed) } if len(block.Bytes) < len(sshsigMagic) || string(block.Bytes[:len(sshsigMagic)]) != sshsigMagic { return nil, fmt.Errorf("%w: missing SSHSIG magic", ErrMalformed) } var sb sshsigBlob if err := ssh.Unmarshal(block.Bytes[len(sshsigMagic):], &sb); err != nil { return nil, fmt.Errorf("%w: %v", ErrMalformed, err) } if sb.Version != 1 { return nil, fmt.Errorf("%w: bad SSHSIG version %d", ErrMalformed, sb.Version) } return &sb, nil } // signedData recomputes the bytes the signature actually covers, per the SSHSIG // spec: "SSHSIG" || ssh.Marshal(namespace, reserved, hash_algorithm, H(message)), // where H is the named hash. The message is the RAW received blob bytes — the // verifier never canonicalizes (the canonical form is the signer's contract). func signedData(sb *sshsigBlob, msg []byte) ([]byte, error) { h, err := hashByName(sb.HashAlgo) if err != nil { return nil, err } h.Write(msg) md := h.Sum(nil) body := ssh.Marshal(struct { Namespace string Reserved string HashAlgo string Hash []byte }{sb.Namespace, sb.Reserved, sb.HashAlgo, md}) return append([]byte(sshsigMagic), body...), nil }