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
- func CanonicalizeSkill(skillDir string) ([]byte, map[string]string, error)
- func ParseSkillName(skillDir string) string
- func VerifyChain(current, previous *SkillSignature) error
- func VerifySkillOffline(skillDir string, disc *discovery.WellKnownResponse, sig *SkillSignature, ...) *verification.VerificationResult
- func VerifySkillOfflineWithDNS(skillDir string, disc *discovery.WellKnownResponse, sig *SkillSignature, ...) *verification.VerificationResult
- func VerifySkillWithResolver(skillDir, domain string, r resolver.SchemaResolver, ...) *verification.VerificationResult
- type ChainError
- type ChainErrorKind
- type SignOptions
- type SkillSignature
- type TamperedFiles
Constants ¶
const SignatureFilename = ".schemapin.sig"
SignatureFilename is the name of the signature file written into skill directories.
Variables ¶
This section is empty.
Functions ¶
func CanonicalizeSkill ¶
CanonicalizeSkill walks a skill directory deterministically and computes a root hash.
Algorithm:
- Recursive sorted directory walk
- Skip .schemapin.sig and symlinks
- Normalize paths to forward slashes
- Per-file: SHA-256(relative_path_utf8 + file_bytes) -> hex -> "sha256:<hex>"
- 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 ¶
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 ¶
func VerifySkillOffline( skillDir string, disc *discovery.WellKnownResponse, sig *SkillSignature, rev *revocation.RevocationDocument, pinStore *verification.KeyPinStore, toolID string, ) *verification.VerificationResult
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 ¶
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.