{"record":{"id":"3aa2436d3ae06be3","repo":"tailscale/tailscale","slug":"package-length-must-be-positive-got-d","errorCode":null,"errorMessage":"package length must be positive, got %d","messagePattern":"package length must be positive, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"clientupdate/distsign/distsign.go","lineNumber":150,"sourceCode":"\t\t\tBytes: []byte(pub),\n\t\t}), nil\n}\n\n// ParseSigningKey parses the PEM-encoded private signing key. The key must be\n// in the same format as returned by GenerateSigningKey.\nfunc ParseSigningKey(privKey []byte) (*SigningKey, error) {\n\tk, err := parsePrivateKey(privKey, pemTypeSigningPrivate)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse root key: %w\", err)\n\t}\n\treturn &SigningKey{k: k}, nil\n}\n\n// SignPackageHash signs the hash and the length of a package. Use PackageHash\n// to compute the inputs.\nfunc (s *SigningKey) SignPackageHash(hash []byte, len int64) ([]byte, error) {\n\tif len <= 0 {\n\t\treturn nil, fmt.Errorf(\"package length must be positive, got %d\", len)\n\t}\n\tmsg := binary.LittleEndian.AppendUint64(hash, uint64(len))\n\treturn ed25519.Sign(s.k, msg), nil\n}\n\n// PackageHash is a hash.Hash that counts the number of bytes written. Use it\n// to get the hash and length inputs to SigningKey.SignPackageHash.\ntype PackageHash struct {\n\thash.Hash\n\tlen int64\n}\n\n// NewPackageHash returns an initialized PackageHash using BLAKE2s.\nfunc NewPackageHash() *PackageHash {\n\th, err := blake2s.New256(nil)\n\tif err != nil {\n\t\t// Should never happen with a nil key passed to blake2s.\n\t\tpanic(err)","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/clientupdate/distsign/distsign.go#L132-L168","documentation":"distsign guards the signature input invariant: SignPackageHash(hash, len) builds the message hash||uint64(len) before Ed25519 signing, and refuses to sign anything with a non-positive length, because a zero- or negative-length package is meaningless and would produce a signature nothing can reproduce (the verifier always derives length from bytes written). The error prints the offending len.","triggerScenarios":"Signing an empty (zero-byte) artifact: PackageHash with nothing written yields Len()==0. Also passing a literal 0 or negative number instead of PackageHash().Len(), or computing the hash before writing the file contents into it.","commonSituations":"Build pipelines that sign placeholder/empty output files before the real artifact is produced; ordering bugs where SignPackageHash runs before the hash is fed; passing a byte-count variable that was never set.","solutions":["Check the artifact is non-empty (stat its size) before signing — an empty file almost always means an upstream build step failed.","Derive len exclusively from the same PackageHash used for the hash: h.Len() after io.Copy(h, file).","If empty artifacts are legitimately possible in your flow, skip signing them explicitly rather than working around the guard.","Audit call order: write data into PackageHash, then Sum() and Len(), then SignPackageHash."],"exampleFix":"// before: signing before hashing, len never populated\nh := distsign.NewPackageHash()\nsig, err := sk.SignPackageHash(h.Sum(nil), 0) // package length must be positive, got 0\n\n// after: hash the file first, then sign with the tracked length\nh := distsign.NewPackageHash()\nf, _ := os.Open(artifact)\nio.Copy(h, f)\nsig, err := sk.SignPackageHash(h.Sum(nil), h.Len())","handlingStrategy":"validation","validationCode":"// guard before signing\nfi, err := os.Stat(artifact)\nif err != nil || fi.Size() <= 0 {\n    return fmt.Errorf(\"refusing to sign empty or missing artifact %s\", artifact)\n}\nh := distsign.NewPackageHash()\nio.Copy(h, mustOpen(artifact))\nsig, err := sk.SignPackageHash(h.Sum(nil), h.Len())","typeGuard":null,"tryCatchPattern":"sig, err := sk.SignPackageHash(hash, n)\nif err != nil && strings.Contains(err.Error(), \"package length must be positive\") {\n    return fmt.Errorf(\"build produced a %d-byte artifact; fix the build step\", n)\n}","preventionTips":["Always source both hash and length from the same PackageHash instance after writing the file.","Fail builds that emit zero-byte artifacts before they reach the signing step."],"tags":["distsign","tailscale","argument-validation","signing","empty-file"],"backgroundTag":"invalid-argument-value","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}