Documentation
¶
Overview ¶
Package clientbuild turns one prebuilt Windows client into a client that knows this team's address.
It does not compile anything. The community edition ships a generic telemetry.exe, and building a client means copying that file and writing the operator's server address into a fixed-size region reserved inside it. That is milliseconds of work, it needs no Go toolchain, and it runs on the cheapest host an operator can rent — which is the whole reason the admin panel can offer a build button at all.
The region ¶
RegionSize bytes, sitting in the client's data section:
[0:16] head marker [16] format version — 0 in a prebuilt binary, 1 once stamped [17] reserved, zero [18:20] payload length, big-endian uint16 [20:24] CRC-32 (IEEE) of the payload, big-endian [24:1008] the payload, [PayloadMax] bytes of room [1008:1024] tail marker
Two markers rather than one, and a scan rather than an offset. A Go binary's layout moves with every rebuild, so an offset recorded today is the wrong offset tomorrow; and a single marker could appear anywhere — inside a compressed asset, inside a string — so the head marker is only a region if the tail marker is exactly RegionSize-16 bytes after it. Find requires exactly one such pair and refuses a file with two, because a file with two is a file this code does not understand well enough to modify.
The payload is versioned, length-prefixed and checksummed so that the client reading it can tell a stamp from the zeroes it was born with, and a good stamp from a half-written one. Its own format is key=value lines, which a client that has never heard of a key can read past.
What stamping does not change ¶
Not the file's size, not its layout, not one byte outside the region. A stamped copy is the prebuilt binary with 1024 bytes rewritten in place, so it is still the same valid PE executable — [Inspect] and [Artifact.Verify] check that rather than assume it.
What it does invalidate is a signature. Any edit to a signed Windows executable breaks its Authenticode signature, which is why signing happens last: Sign is given the stamped copy, and nothing touches the bytes after it returns.
Signing ¶
An operator who gives this server a certificate gets every client it builds signed with it, in the same press of the button. The certificate is a PKCS#12 file they upload, held sealed in the database; the signature is Authenticode, built here rather than shelled out to a tool that does not exist on the hosts these servers run on.
A signature says who built the file and that nobody has changed it since. Whether it stops Windows warning the driver is a separate question and belongs to the certificate rather than to this code: one from a public authority stops it once the file has a download reputation, and one the operator issued themselves stops it only on machines that trust them.
Signatures made here are not timestamped. A timestamp is what keeps a signature valid after the certificate behind it expires, and getting one means asking a timestamping authority over the network — which this server otherwise never does. Until that is a thing an operator opts into, a client stops verifying on the day the certificate expires, and the build page says so.
Index ¶
- Constants
- Variables
- func Find(binaryFile []byte) (int, error)
- func Sign(body []byte, cert Certificate, opus Opus) ([]byte, error)
- func ValidReference(s string) bool
- func Write(binaryFile []byte, s Stamp) error
- type Artifact
- type Builder
- type Certificate
- type CertificateInfo
- type Client
- type Opus
- type Request
- type Signing
- type Source
- type Stamp
Constants ¶
const ( // RegionSize is the whole reserved region, markers included. RegionSize = 1024 // MarkerSize is the length of each marker. MarkerSize = 16 // HeaderSize is what sits between the head marker and the payload: the // version, one reserved byte, the length and the checksum. HeaderSize = 8 // PayloadMax is how many bytes of payload the region has room for. PayloadMax = RegionSize - 2*MarkerSize - HeaderSize )
The shape of the region. Every offset here is also written down in the client that reads it (github.com/pacenote-sim/telemetry/internal/stamp); the two halves are separate programs in separate repositories, so this is a contract and not an implementation detail. Changing a number here means changing it there and giving the format a new version.
const ( // VersionBlank is the region of a prebuilt client: room reserved, nothing // written into it. VersionBlank = 0 // Version1 is the key=value payload this build writes and the shipped // client reads. Version1 = 1 )
The format versions.
const ( // KeyAddress is the server address the client talks to. KeyAddress = "url" // KeyBuild identifies the build that wrote the stamp, so a driver's // support question can be traced back to a row in the build history. KeyBuild = "build" )
The payload keys of Version1.
const MaxClientBytes = 256 << 20
MaxClientBytes is the largest prebuilt client this server will read into memory. The real one is a little over ten megabytes; the limit is here so that a path pointing at something enormous by mistake is an error message rather than a server that stops answering.
const ReferenceBytes = 8
ReferenceBytes is how much randomness a build's reference carries. It names the file on disk and travels in the download address, so it is wide enough that one cannot be guessed from another.
Variables ¶
var ( // ErrNoRegion reports that a file carries no reserved region. It is what a // client built before stamping existed looks like, and the panel says so // rather than writing into a file it does not recognise. ErrNoRegion = errors.New("clientbuild: this binary has no reserved region") // ErrManyRegions reports that a file carries more than one. Nothing should // produce such a file, and the safe thing to do with one is refuse it. ErrManyRegions = errors.New("clientbuild: this binary has more than one reserved region") // ErrBlank reports that a region has never been stamped. ErrBlank = errors.New("clientbuild: this binary carries no stamp") // ErrCorrupt reports a region that does not check out. ErrCorrupt = errors.New("clientbuild: the stamp in this binary is not readable") // ErrTooLong reports a payload with no room in the region. ErrTooLong = errors.New("clientbuild: the stamp does not fit in the reserved region") )
The errors a caller acts on.
var ErrCertificate = errors.New("clientbuild")
ErrCertificate reports a certificate this server cannot sign with. The message is the operator's, because every cause is something they fix in the file they uploaded.
var ErrNoCertificate = errors.New("clientbuild: this server has no certificate to sign with")
ErrNoCertificate reports that a signed client was asked for on a server with no certificate to sign it with.
var ErrNotPE = errors.New("clientbuild: that is not a Windows executable this server can sign")
ErrNotPE reports a file this package cannot read as a Windows executable. It is separate from the PE parsing Source does because signing needs offsets that debug/pe does not expose.
var ErrNotReady = errors.New("clientbuild: there is no prebuilt client to build from")
ErrNotReady reports that there is nothing to build from: no prebuilt client where this server was told to look, or one it cannot stamp.
Functions ¶
func Find ¶
Find returns the offset of the reserved region in a binary.
A candidate is a head marker with a tail marker exactly RegionSize-16 bytes after it. Anything else that happens to hold the head marker — a string in the client's own code, a byte sequence inside a compressed asset — is not a region and is passed over.
func Sign ¶
func Sign(body []byte, cert Certificate, opus Opus) ([]byte, error)
Sign signs a stamped client with the operator's certificate and returns the signed file.
It must be the last thing done to the bytes. Every edit to a signed Windows executable breaks its signature, which is why stamping happens first and why nothing here writes into the program.
func ValidReference ¶
ValidReference reports whether a string is shaped like a reference this package mints: hexadecimal, and exactly as long as one.
func Write ¶
Write stamps a binary in place.
It writes RegionSize bytes and nothing else: the file that comes back is the same length, the same layout and byte-identical everywhere outside the region. The caller owns the slice, which is a copy of the prebuilt client and never the prebuilt client itself.
Types ¶
type Artifact ¶
type Artifact struct {
// Reference identifies this build. It is the file's name on disk and the
// last part of its download address.
Reference string
// Name is what the file is called when a driver downloads it.
Name string
// Path is where it is on this server.
Path string
// Size is the built file's size. Stamping does not change it; signing
// does, by the length of the signature put on the end.
Size int64
// SHA256 is the hex digest of the built file, which is what an operator
// tells their drivers to check.
SHA256 string
// Address is read back out of the file after it was written, so the page
// shows what the client will actually do rather than what was asked for.
Address string
// Signing is the route it was built down.
Signing Signing
// SignedBy is the certificate that signed it, for the page to show and the
// history to record. Empty for an unsigned build.
SignedBy string
// Client is the prebuilt client it was stamped from.
Client Client
// At is when it was built.
At time.Time
}
Artifact is a built client: the file, and everything the page says about it.
type Builder ¶
type Builder struct {
// Source is the prebuilt client.
Source Source
// Dir is where built clients are written. Empty means this server cannot
// keep one, which [Builder.Build] refuses rather than building into
// nowhere.
Dir string
// Now is the clock, injectable for tests.
Now func() time.Time
// Certificate is the operator's own code-signing certificate, or nil when
// they have not given this server one.
//
// It is asked for at the moment a client is built rather than held, for
// two reasons. An operator uploads and removes a certificate while the
// server runs, so a captured value would go stale; and the private key is
// sealed in the database, so holding it would mean holding an opened
// private key in memory for the life of the process to serve a button
// somebody presses once a month.
Certificate func(context.Context) (*Certificate, error)
}
Builder makes clients out of the prebuilt one.
func (Builder) Build ¶
Build copies the prebuilt client, writes the address into the copy, and checks the result before it is offered to anyone.
The check is the point of doing it this way round. The copy is verified to be the same size as the original, byte-identical outside the region, still a PE executable, and to read back the address that was asked for. A file that fails any of those is not written, because the next thing that happens to it is that a team's drivers install it.
func (Builder) Exists ¶
Exists reports whether the file for this build is still on the server. The history outlives the files — an operator can clear the directory, and a build made on a machine that has since been rebuilt is a row with nothing behind it — so the page asks rather than offering a link into nothing.
func (Builder) Open ¶
Open returns the built client with this reference, for the download route. A reference that is not one this package minted is refused before it reaches the file system, so the address bar cannot be used to read another file.
type Certificate ¶
type Certificate struct {
// Leaf is the certificate that signs.
Leaf *x509.Certificate
// Chain is everything between the leaf and a root, which travels with the
// signature so that a machine trusting the root can build the path.
Chain []*x509.Certificate
// Key signs.
Key crypto.Signer
}
Certificate is the operator's code-signing certificate and its key.
The key never leaves this process. It is held as a crypto.Signer rather than as key material so that the day this grows to speak to a hardware token or a cloud key service, nothing above this line changes.
func ParseCertificate ¶
func ParseCertificate(pfx []byte, password string) (Certificate, error)
ParseCertificate opens a PKCS#12 file — a .pfx or .p12, which is what every certificate authority hands out and what Windows exports — and checks that what came out of it can actually sign.
Every failure here is the operator's to fix and is phrased as such. The commonest by a distance is the wrong password, and it is worth saying which of the two things went wrong: a password that is merely wrong and a file that is not a certificate store at all need different fixes.
func (Certificate) Describe ¶
func (c Certificate) Describe() CertificateInfo
Describe is what the page says about this certificate.
type CertificateInfo ¶
type CertificateInfo struct {
// Subject and Issuer are who the certificate says signed and who vouched
// for them, as a person reads them.
Subject, Issuer string
// NotBefore and NotAfter are the window the signature is made inside.
NotBefore, NotAfter time.Time
// Thumbprint is the SHA-256 of the certificate, which is what an operator
// compares against what their authority issued them.
Thumbprint string
// Algorithm is the key, in words: "RSA 3072" or "ECDSA P-256".
Algorithm string
// SelfSigned is whether the operator issued it to themselves, which is the
// normal case here and changes what their drivers will see.
SelfSigned bool
// CodeSigning is whether the certificate is marked for signing software.
// A certificate without it can still make a signature and Windows will not
// accept it, so the page says so rather than letting an operator find out
// from a driver.
CodeSigning bool
}
CertificateInfo is everything the build page says about a stored certificate. It is derived from the certificate and holds no key material, so it is safe to put on a page and in a log.
type Client ¶
type Client struct {
// Path is where this server looked.
Path string
// Present is whether there was a file there.
Present bool
// Size and ModifiedAt are the file's own facts.
Size int64
ModifiedAt time.Time
// SHA256 is the hex digest of the prebuilt file, so an operator can check
// that what they copied onto the server is what they downloaded.
SHA256 string
// Module, Version, Revision and BuiltAt come from the build information
// the Go toolchain records inside every binary it links. They are how the
// page answers "which version is this" without anybody maintaining a
// version file beside the executable.
Module string
Version string
Revision string
BuiltAt time.Time
// GOOS and GOARCH are what the client was built for, read from the same
// place. A macOS build sitting in the Windows client's path is a mistake
// worth naming before an operator hands it to a driver.
GOOS, GOARCH string
// Machine is the processor the PE header names, in words.
Machine string
// Stampable is whether this server can build clients from this file.
Stampable bool
// StampedWith is the address the file already carries, which a prebuilt
// client normally does not. It is shown rather than hidden: a generic
// client that already points somewhere is worth knowing about.
StampedWith string
// Problem is why it cannot be used, in the words the operator needs.
// Empty when there is nothing wrong.
Problem string
}
Client is everything the build page says about the prebuilt client: where it came from, which build it is, and whether it can be stamped.
It is a value that always renders. A server with no client binary yet is the normal state of a fresh installation, and the page has to be able to say so plainly — so "not there" is a field and not an error.
func (Client) Describes ¶
Describes reports whether the Go toolchain's build information was readable, which is what the version on the page comes from.
func (Client) ShortRevision ¶
ShortRevision is the first twelve characters of the commit, which is what a person compares.
type Opus ¶
type Opus struct {
// Program is the program's name.
Program string
// URL is where a driver can read about it.
URL string
}
Opus is what Windows shows a driver about the program itself, beside who signed it. Both parts are optional and both are worth filling in: the dialog a driver sees says "unknown" for anything left out.
type Request ¶
type Request struct {
// Address is the server the built client will talk to.
Address string
// Signing is the route the operator chose.
Signing Signing
// Program is what Windows calls this client in the dialog it shows a
// driver who runs it. It is the operator's own organisation, so that a
// driver in two teams can tell whose client they are about to install.
// Empty leaves the field out, which Windows renders as "unknown".
Program string
}
Request is one press of the build button.
type Signing ¶
type Signing string
Signing is how a built client is signed, which is the one question on the build page an operator has to think about.
There are two, and an operator has to be shown both: the difference between them is the difference between a driver seeing a Windows warning and not.
A third once lived here — a paid service that built and signed on our own infrastructure. It has been taken out rather than left disabled. A choice an operator can read, understand and never use is a choice that wastes their attention every time they visit the page, and offering to sell something that does not exist is worse than not mentioning it.
const ( // SigningNone is a stamped copy of the prebuilt client, handed over as it // is. Free, immediate, and every driver sees Windows warn them. SigningNone Signing = "unsigned" // SigningOwnCertificate is the operator signing with a certificate of // their own, which this server holds. SigningOwnCertificate Signing = "own-certificate" )
The two routes.
func SigningRoutes ¶
func SigningRoutes() []Signing
SigningRoutes is the two in the order the page offers them.
func (Signing) Available ¶
Available reports whether this server can take this route right now.
It takes an argument rather than reading anything because the answer changes while the server runs: an operator uploads a certificate on the same page they build from, and the route goes from unavailable to available without a restart.
func (Signing) Unavailable ¶
Unavailable is why a route cannot be taken, phrased for the operator who just chose it. It is empty for a route that can be.
type Source ¶
type Source struct {
// Path is the file. Empty means this server has not been told where the
// client is, which the page says rather than guessing.
Path string
}
Source is the prebuilt Windows client this server stamps copies of.
func (Source) Inspect ¶
Inspect reads the prebuilt client and reports what it is.
It never returns an error. Every way this can go wrong is something the operator has to fix in the file system — a missing file, the wrong file, a file this server may not read — and each of those is a sentence on the page rather than a stack trace in a log.
It reads the whole file and digests it, which is tens of milliseconds on the eleven megabytes the real client weighs. That is a page an operator opens rarely, and the digest is the reason they opened it: it is how they check that what they copied onto the server is what they downloaded.
type Stamp ¶
type Stamp struct {
// Address is the server the built client talks to: a scheme and a host,
// with no trailing slash.
Address string
// Build identifies the build this copy came from, or is empty.
Build string
}
Stamp is what is written into the region, and what reads back out of it.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package clientbuildtest builds the fixtures the client-building tests need: a file shaped like a Windows executable, with a reserved region inside it.
|
Package clientbuildtest builds the fixtures the client-building tests need: a file shaped like a Windows executable, with a reserved region inside it. |