skill

package
v0.0.0-...-740378c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 1, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package skill provides skill folder signing and verification for SchemaPin v1.3 with v1.4-alpha additions.

Extends SchemaPin's ECDSA P-256 signing to cover file-based skill folders (AgentSkills spec). Same keys, same .well-known discovery, new canonicalization target.

v1.4-alpha additions:

  • Optional signature expiration (expires_at) on .schemapin.sig -- written via SignSkillWithOptions. Verifiers degrade past expires_at instead of failing.
  • Optional DNS TXT cross-verification via VerifySkillOfflineWithDNS; see the dns subpackage for the parser and lookup helpers.

Index

Constants

View Source
const SignatureFilename = ".schemapin.sig"

SignatureFilename is the name of the signature file written into skill directories.

Variables

This section is empty.

Functions

func CanonicalizeSkill

func CanonicalizeSkill(skillDir string) ([]byte, map[string]string, error)

CanonicalizeSkill walks a skill directory deterministically and computes a root hash.

Algorithm:

  1. Recursive sorted directory walk
  2. Skip .schemapin.sig and symlinks
  3. Normalize paths to forward slashes
  4. Per-file: SHA-256(relative_path_utf8 + file_bytes) -> hex -> "sha256:<hex>"
  5. Root: sort manifest keys, extract hex digests, concatenate, SHA-256 -> raw bytes

Returns (root_hash_bytes, manifest, error). Returns error if directory is empty.

func ParseSkillName

func ParseSkillName(skillDir string) string

ParseSkillName extracts the skill name from SKILL.md frontmatter. Falls back to the directory basename if SKILL.md is missing or has no name field.

func VerifyChain

func VerifyChain(current, previous *SkillSignature) error

VerifyChain verifies that current is the legitimate successor of previous via the previous_hash lineage chain (v1.4 alpha.2).

Checks current.PreviousHash == previous.SkillHash.

This is a pure-metadata check -- no cryptography is re-evaluated. Both signatures must already be cryptographically verified separately via VerifySkillOffline for the chain check to be meaningful.

Use this to defend against rug-pull attacks where an attacker substitutes a schema/skill out-of-band: a legitimate update declares the prior version's hash; an unauthorized substitution either omits previous_hash or points at a hash the verifier has not accepted as a valid ancestor.

func VerifySkillOffline

VerifySkillOffline verifies a signed skill folder offline using pre-fetched discovery and revocation data. Follows the 7-step verification flow.

func VerifySkillOfflineWithDNS

func VerifySkillOfflineWithDNS(
	skillDir string,
	disc *discovery.WellKnownResponse,
	sig *SkillSignature,
	rev *revocation.RevocationDocument,
	pinStore *verification.KeyPinStore,
	toolID string,
	dnsTxt *dns.DnsTxtRecord,
) *verification.VerificationResult

VerifySkillOfflineWithDNS performs the standard offline verification flow and then -- when dnsTxt is non-nil -- cross-checks the DNS TXT record's fingerprint against the discovery key.

Behaviour mirrors the Rust verify_skill_offline_with_dns:

  • dnsTxt == nil -> identical to VerifySkillOffline
  • underlying verification fails -> returned as-is, no DNS check
  • DNS fingerprint matches -> result returned unchanged
  • DNS fingerprint mismatches -> failed result with ErrDomainMismatch

DNS TXT cross-verification is an optional, additive trust signal: an absent record never causes a failure, but a present-and-mismatched record is a hard failure (the second-channel check exists precisely to catch HTTPS-side compromise).

func VerifySkillWithResolver

func VerifySkillWithResolver(
	skillDir, domain string,
	r resolver.SchemaResolver,
	pinStore *verification.KeyPinStore,
	toolID string,
) *verification.VerificationResult

VerifySkillWithResolver verifies a signed skill folder using a resolver for discovery and revocation.

Types

type ChainError

type ChainError struct {
	Kind     ChainErrorKind
	Expected string
	Got      string
}

ChainError is returned by VerifyChain on lineage failure.

func (*ChainError) Error

func (e *ChainError) Error() string

type ChainErrorKind

type ChainErrorKind int

ChainErrorKind enumerates VerifyChain failure modes.

const (
	// ChainErrorNoPreviousHash indicates current.PreviousHash is empty.
	ChainErrorNoPreviousHash ChainErrorKind = iota + 1
	// ChainErrorMismatch indicates current.PreviousHash != previous.SkillHash.
	ChainErrorMismatch
)

type SignOptions

type SignOptions struct {
	// SignerKid overrides the kid written into the signature. When empty,
	// the kid is derived from the public key fingerprint.
	SignerKid string
	// SkillName overrides the skill_name written into the signature. When
	// empty, it is parsed from SKILL.md frontmatter or falls back to the
	// directory basename.
	SkillName string
	// ExpiresIn sets a TTL relative to signing time. When > 0, the
	// signature carries an RFC 3339 expires_at field and the version is
	// bumped to "1.4". A zero value (the default) writes no expires_at and
	// keeps the version at "1.3".
	ExpiresIn time.Duration
	// SchemaVersion is a caller-supplied semver string identifying *this*
	// version of the signed artifact (v1.4 alpha.2). Empty omits the field.
	SchemaVersion string
	// PreviousHash is sha256:<hex> of the prior signed version's SkillHash,
	// forming a hash chain (v1.4 alpha.2). Pair with VerifyChain at verify
	// time. Empty omits the field.
	PreviousHash string
}

SignOptions are optional sign-time parameters for SignSkillWithOptions.

All fields are optional and default to "absent": empty strings derive the value from the key/SKILL.md/dirname, and a zero ExpiresIn omits the expires_at field entirely.

type SkillSignature

type SkillSignature struct {
	SchemapinVersion string            `json:"schemapin_version"`
	SkillName        string            `json:"skill_name"`
	SkillHash        string            `json:"skill_hash"`
	Signature        string            `json:"signature"`
	SignedAt         string            `json:"signed_at"`
	ExpiresAt        string            `json:"expires_at,omitempty"`
	SchemaVersion    string            `json:"schema_version,omitempty"`
	PreviousHash     string            `json:"previous_hash,omitempty"`
	Domain           string            `json:"domain"`
	SignerKid        string            `json:"signer_kid"`
	FileManifest     map[string]string `json:"file_manifest"`
}

SkillSignature represents the JSON structure of a .schemapin.sig file.

Optional v1.4 fields:

  • ExpiresAt: when present, verifiers treat signatures past the expiration as degraded (warning) rather than a hard failure -- see VerifySkillOffline.
  • SchemaVersion: caller-supplied semver string identifying *this* version of the signed artifact. Surfaced via VerificationResult for policy use.
  • PreviousHash: sha256:<hex> of the prior signed version's SkillHash, forming a hash chain. Pair with VerifyChain.

func LoadSignature

func LoadSignature(skillDir string) (*SkillSignature, error)

LoadSignature reads and parses the .schemapin.sig file from a skill directory.

func SignSkill

func SignSkill(skillDir, privateKeyPEM, domain string, signerKid, skillName string) (*SkillSignature, error)

SignSkill canonicalizes a skill directory, signs it, and writes .schemapin.sig.

If signerKid is empty, it is auto-computed from the public key. If skillName is empty, it is parsed from SKILL.md (or falls back to dir name).

Preserved as a thin wrapper over SignSkillWithOptions for backward compatibility with v1.3 callers.

func SignSkillWithOptions

func SignSkillWithOptions(skillDir, privateKeyPEM, domain string, options SignOptions) (*SkillSignature, error)

SignSkillWithOptions canonicalizes a skill directory, signs it, and writes .schemapin.sig. Mirrors the v1.4 Rust API sign_skill_with_options.

When options.ExpiresIn > 0, an RFC 3339 expires_at timestamp is written (truncated to seconds, UTC, "Z" suffix) and the schemapin_version is bumped to "1.4". When ExpiresIn is zero, expires_at is omitted and the version stays at "1.3" -- bytewise-identical to the v1.3 wire format.

type TamperedFiles

type TamperedFiles struct {
	Modified []string
	Added    []string
	Removed  []string
}

TamperedFiles holds the result of comparing two file manifests.

func DetectTamperedFiles

func DetectTamperedFiles(current, signed map[string]string) *TamperedFiles

DetectTamperedFiles compares a current file manifest against a signed manifest. Returns a TamperedFiles struct with sorted Modified, Added, and Removed slices.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL