# Code signing

The installer is unsigned, and it costs us. VirusTotal on 0.1.12:

    clipshelf-0.1.12.zip          0/75   clean
    clipshelf-setup-0.1.12.exe    4/75   APEX, DeepInstinct, Skyhigh, MICROSOFT

Microsoft is the one that matters, because Defender runs by default on every
Windows machine. `Trojan:Win32/Wacatac.C!ml` is its machine-learning bucket,
not a signature match, and it is the single most-reported false positive
among independent developers. Every property that trips it is a property of
this installer being what it is:

- **unsigned** (verified: no Authenticode certificate table at all)
- **payload entropy 8.00 bits/byte** -- lzma2/max compression is
  indistinguishable from encryption to a scanner, which is exactly what
  Skyhigh's "ObfuscatedPoly" means
- **drops an interpreter and runs it** (`Exec(python.exe, first_run.py)`)
- **downloads and executes code during install** -- get-pip.py, then
  arbitrary PyPI packages, then an 80 MB ffmpeg binary
- **zero prevalence** -- and every release resets that to zero

The zip contains the same application code and scores 0/75, which is the
proof that none of this is about what the app does.

Signing is the fix: reputation attaches to the publisher rather than to each
new file, so it does not reset every release.

## Azure Artifact Signing

Formerly "Trusted Signing"; renamed in 2026. The Azure resource provider is
still called `Microsoft.CodeSigning`, and much of the tooling still says
"CodeSigning", so both names appear below.

**$9.99/month** (Basic, 5,000 signatures). This replaced the old
$200-400/year OV certificate market, which the 2023 CA/Browser Forum rules
killed by requiring HSM-backed keys.

**Individual developers must be in the United States or Canada.** Businesses
have a longer list. There is no way around this one.

### The part that is easy to get wrong

For an individual, the certificate's name and address are **taken from your
Azure billing account**, not from a form you fill in:

- the billing account's **Account Type must be "Individual"** -- an
  organisation billing account cannot do individual validation, or vice versa
- the **legal name and address must match your government ID exactly**,
  because that is what ends up on the certificate
- fixing a mistake afterwards means a **new identity validation request**,
  which invalidates certificates already issued from the old one

Set the billing account details right *before* starting validation.

### Setting it up

All of this is in the Azure portal at https://portal.azure.com. There is an
`az` command line too, but it is a separate install and it cannot do the
identity validation anyway, so the portal is the shorter road. Everything
below is the search box at the top of the portal plus a few clicks.

**0. Check the billing account first.** Search **Cost Management + Billing**
-> **Billing scopes** / **Properties**. You want:

- Account Type: **Individual**
- Legal name and address matching your government ID **exactly**

This is not a formality. For an individual, the certificate's name and
address are taken from here rather than from a form you fill in, and
changing it afterwards means redoing the identity validation, which
invalidates any certificate already issued from it.

**1. Turn the service on for your subscription.** Search **Subscriptions**
-> your subscription -> **Resource providers** (left menu, under Settings)
-> search `Microsoft.CodeSigning` -> select it -> **Register**. It goes from
NotRegistered to Registered in a few seconds.

**2. Create the account.** Search **Artifact Signing Accounts** -> **Create**.

- Resource group: create one, any name (`clipshelf-signing` is fine)
- Account name: 3-24 characters, globally unique, starts with a letter
- Region: pick one and **write it down** -- it has to match the endpoint in
  signing.json later
- Pricing: **Basic**

**3. Give yourself permission to verify identity.** On the new account ->
**Access control (IAM)** -> **Add** -> **Add role assignment** -> role
**Artifact Signing Identity Verifier** -> Members -> yourself -> Review +
assign.

Do this before the next step. Without it the "New identity" button is simply
greyed out, with nothing saying why.

**4. Prove who you are.** On the account -> **Identity validations** ->
change the dropdown from Organization to **Individual** -> **New Identity**
-> **Public**.

Pick your billing account; the form fills itself in and is read-only. Check
the **Certificate subject preview** -- that is what will appear on the
certificate.

Status goes to In Progress, then **Action Required**. Click your name, then
the verification link. You will need:

- a **mobile device** with **Microsoft Authenticator** installed
- a **government photo ID** (passport, driving licence, or state ID -- not a
  library card, student card, or club membership)
- a **phone number**

The ID check runs through AU10TIX: email PIN, phone number, then scan a QR
code with your phone and photograph your ID. For an individual this takes
minutes. (The "1 to 20 business days" in Microsoft's docs is the
organisation path, which involves a human reviewing business records.)

Status returns to **Completed** when it works.

**5. Create the certificate profile.** On the account -> **Certificate
profiles** -> **Create** -> **Public Trust**. Give it a name, pick the
identity validation you just completed under **Verified CN and O**.

**6. Give yourself permission to sign.** Access control (IAM) again ->
**Add role assignment** -> **Artifact Signing Certificate Profile Signer**
-> yourself. Identity Verifier does NOT let you sign; they are separate
roles and having the first does not imply the second.

If you would rather use commands, the portal has a terminal built in -- the
`>_` icon in the top bar opens Cloud Shell with `az` already installed and
already logged in, so there is nothing to set up. `az provider register
--namespace Microsoft.CodeSigning` and `az artifact-signing create` do steps
1 and 2. Steps 3 to 6 are quicker in the UI, and step 4 is portal-only.

### When identity validation fails

**"Address didn't match verified ID"** even though they plainly do. Two
causes, both non-obvious:

1. **The request captured the billing address when it was created, and is
   immutable.** It does not re-read it. So if the billing address was
   corrected after the request was made, the request is still comparing
   against the old one, and the portal shows you the new one. Look at what
   the FAILED request captured, not at what Cost Management shows today.

2. **AU10TIX reads the address printed on the ID and ignores any recorded
   change of address.** A licence carrying an old address fails even if the
   DMV record, and everything else, says otherwise.

Also worth checking character by character, because these are compared
strictly: `St` against `Street`, `N` against `North`, a unit number present
in one and absent in the other, ZIP+4 against a five-digit ZIP.

**A full state name in the portal against a two-letter code on the ID is
probably NOT the problem.** Every US licence prints the abbreviation and the
portal only offers full names, so if that combination were fatal no
individual in the country could onboard. The CLI's sold-to field takes
`region="GA"`, which suggests the portal is displaying a stored code rather
than storing what it displays. Check what is actually stored before chasing
it -- in Cloud Shell (the `>_` icon in the portal, nothing to install):

    az billing account list --query "[].{name:name, agreement:agreementType, soldTo:soldTo}"

If it really does need changing, the portal cannot alter casing or hyphens
but the CLI can:

    az billing account update --name "<billingAccountName>" \
        --sold-to first-name="..." last-name="..." address-line1="..." \
        city="..." region="GA" postal-code="..." country="US"

Two caveats: this works only on billing accounts with agreement type
**Microsoft Customer Agreement**, and some accounts need manual approval
before the sold-to can change at all, in which case it tells you to contact
support.

**Check what AU10TIX actually extracted** before assuming the fault is in
the billing address. On a Georgia licence it read the COUNTY field
("BARROW") into the address slot, so the Verified ID carried a county name
where a street address should be. No amount of correcting the billing
account can match that.

If the extraction is wrong:

- **Delete the Verified ID from Microsoft Authenticator first.** It is stored
  as a credential, so presenting it again just re-presents the same bad data
  and every retry fails identically. Authenticator -> Verified IDs -> remove.
- Re-capture carefully: flat on a surface, photographed from directly above,
  no flash, out of direct sunlight, uncropped with margins on all four sides,
  one image per side as separate files, nothing covering the document.
- If a clean capture still extracts the wrong field, it is a template bug
  rather than a photo problem, and re-scanning will not fix it. A state ID
  card may hit a different template; a passport will not help, since it
  carries no address at all.
- Escalate quoting the extracted value and which field it actually came
  from. "It read the county into the address" is reproducible; "it does not
  work" is not.

The fix, in order:

1. Correct the billing (**sold-to**) address in **Cost Management + Billing**
   and wait for it to be **fully approved** -- some changes need Microsoft to
   verify them, and creating a request before that just repeats the failure.
2. Create a **brand new** identity validation request. The failed one cannot
   be repaired.
3. If AU10TIX still rejects the ID because its printed address is genuinely
   out of date, supply a **utility bill or bank statement** instead: recent
   (within about three months), showing name, address and date on the first
   page.
4. Use the **same email address** at every step of the flow.

If a fresh request still fails with correct billing data, escalate it as an
**"Artifact Signing onboarding / account verification problem"**, not as
general technical support. Billing and subscription support is included for
free; this gets miscategorised into the paid tier if you describe it as a
technical issue, which is a thing that has happened to people.

### On the build machine

    winget install -e --id Microsoft.Azure.ArtifactSigningClientTools

That pulls SignTool, the .NET 8 runtime, the VC++ redistributable and the
signing dlib together. SignTool must be **10.0.2261.755 or newer**; the
20348 Windows SDK is explicitly not supported by the dlib.

Then copy `installer/signing.example.json` to `installer/signing.json` and
fill it in. That file is gitignored: it holds no secret, but it is specific
to one machine and one account.

    installer\build_local.ps1 -Sign

### Timestamping is not optional

Artifact Signing certificates are valid for about **three days**. The
signature outlives them only because it is timestamped, so the build script
always passes `/tr http://timestamp.acs.microsoft.com`. Without it every
installer stops verifying three days after it was built, which would look
like a corrupted download.

## Afterwards

- Re-submit to VirusTotal (`scripts/virustotal.py`) and compare.
- Delete the "Windows will warn you" callout from `site/index.html` -- the
  `.warn` block -- once SmartScreen stops firing. Reputation still has to
  accumulate, so give it some downloads first; signing does not clear
  SmartScreen the instant the first signed build ships.
- Report the current false positive to Microsoft regardless, at
  https://www.microsoft.com/en-us/wdsi/filesubmission -- free, and it helps
  the unsigned builds already in people's hands.
