Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .buildkite/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# This directory not used by base Julia!

This directory is for performing CI on the `julia-buildkite` repository itself! It does not get used at all during the actual build for Julia itself.
This directory is for performing CI on the `julia-buildkite` repository
itself! It does not get used at all during the actual build for Julia
itself.

## How the self-test works

The `julia-buildkite-ci` Buildkite pipeline (untrusted; same cluster as
`julia-pr` / `julia-ci`) builds this repository's pull requests and
`main`. Its webUI configuration is the same "Launch pipelines" step as
the julia build pipelines (see `pipelines/main/0_webui.yml`), but the
pipeline's *repository* is `JuliaCI/julia-buildkite` -- so every job of
the build checks out this repository and runs `hooks/post-checkout`,
which:

1. replaces the working directory with a `JuliaLang/julia` checkout
(`UPSTREAM_BRANCH`, default `master`; pinned once per build via
meta-data so all jobs test the same julia commit), and
2. pins the external-buildkite plugin's build meta-data to
`${BUILDKITE_COMMIT}` (this build's julia-buildkite commit), so every
launched job runs the proposed pipeline code.

Trust model (see `ops/README.md`): the self-test pipeline holds no
secrets, tokens, or signing rights. Build jobs stage their artifacts
write-once to the pipeline's own ephemeral bucket
(`julialang-ephemeral-buildkite`) via its own OIDC role
(`julia-oidc-stage-buildkite`) -- separate from the buckets juliaup and
julia-publish consume, so a self-test build can never place anything a
production consumer would read. The julia-publish trigger is gated on
`pipeline.slug == "julia-ci"` and so never fires here.
1 change: 0 additions & 1 deletion .buildkite/cryptic_repo_root/.buildkite

This file was deleted.

2 changes: 0 additions & 2 deletions .buildkite/cryptic_repo_root/README.md

This file was deleted.

25 changes: 17 additions & 8 deletions .buildkite/hooks/post-checkout
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
#!/usr/bin/env bash

# Repository hook for the julia-buildkite SELF-TEST pipeline: its jobs
# check out this repository, and this hook then (1) swaps the working
# directory for a JuliaLang/julia checkout (pinned once per build via
# meta-data, so every job tests the same julia commit) and (2) points the
# external-buildkite plugin's meta-data at this build's commit -- so the
# whole build/test flow runs against the proposed julia-buildkite code.
# See .buildkite/README.md.

set -euo pipefail

# Buildkite users can trigger new builds with these values overridden to
# test specific julia gitshas
UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/JuliaLang/julia.git}"
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-release-1.13}"
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-master}"

# All of our workers are required to provide a default for the julia cache dir
# We're going to cache repositories in here, just like buildkite itself would:
UPSTREAM_CACHE="${BUILDKITE_PLUGIN_JULIA_CACHE_DIR?}/repos/$(tr ':/.' '---' <<<"${UPSTREAM_URL}")"
# Cache repositories in the worker's julia cache dir (if it provides one),
# just like buildkite itself would:
UPSTREAM_CACHE="${BUILDKITE_PLUGIN_JULIA_CACHE_DIR:-}/repos/$(tr ':/.' '---' <<<"${UPSTREAM_URL}")"

DISABLE_UPSTREAM_CACHE="false"
if [[ "$(uname)" == "FreeBSD" ]]; then
if [[ "$(uname)" == "FreeBSD" || -z "${BUILDKITE_PLUGIN_JULIA_CACHE_DIR:-}" ]]; then
DISABLE_UPSTREAM_CACHE="true"
fi

Expand Down Expand Up @@ -96,6 +104,7 @@ if ! buildkite-agent meta-data exists BUILDKITE_JULIA_VERSION; then
buildkite-agent meta-data set BUILDKITE_JULIA_BRANCH "${UPSTREAM_BRANCH}"
fi

# Export S3_BUCKET and S3_BUCKET_PREFIX, to force our CI to upload to a different S3 target than the actual CI pipeline would.
export S3_BUCKET="julialang-ephemeral"
export S3_BUCKET_PREFIX="julia-buildkite-uploads/bin"
# No S3 redirection is needed here: staging-bucket selection is keyed on
# the pipeline slug (utilities/build_envs.sh), so self-test jobs stage to
# the pipeline's own ephemeral bucket, and IAM only grants this pipeline's
# OIDC identity that bucket anyway (see ops/terraform/iam.tf).
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ cryptic-buildkite-plugin/
# Ignore IDE files
.vscode/

__pycache__/

# Terraform working directories (provider binaries); lock files are committed
.terraform/
*.tfstate
*.tfstate.*
12 changes: 0 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,2 @@
export PATH := $(shell pwd)/cryptic-buildkite-plugin/bin:$(PATH)

decrypt:
cd .buildkite/cryptic_repo_root && decrypt --repo-root=$$(pwd) --verbose

sign_treehashes:
cd .buildkite/cryptic_repo_root && sign_treehashes --repo-root=$$(pwd) --verbose
git commit -a -m "sign treehashes"

verify_treehashes:
cd .buildkite/cryptic_repo_root && verify_treehashes --repo-root=$$(pwd) --verbose

print-%:
echo "$*=$($*)"
5 changes: 0 additions & 5 deletions cryptic_repo_keys/.gitignore

This file was deleted.

6 changes: 0 additions & 6 deletions cryptic_repo_keys/README.md

This file was deleted.

Binary file removed cryptic_repo_keys/repo_key.2297e5e7
Binary file not shown.
72 changes: 29 additions & 43 deletions devdocs/macos-developer-id.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,40 @@
## Expired certificates

The main symptom will be that the upload job will be failing. The unlock
keychain step prints the status of the certificate and it will print something
like (`CSSMERR_TP_CERT_EXPIRED`) if the certificate is expired.

### Update expired Apple Developer ID

To replace the certificate you will need first a MacOS machine and to emit a new
certificate.

1. To replace an expired Apple certificate, clone `julia-buildkite` repo and
clone https://github.com/JuliaCI/cryptic-buildkite-plugin in its root.
You will also need to have the private `agent.key`, and point towards it
by defining the `AGENT_PRIVATE_KEY_PATH` environment variable.
2. You can decrypt by running `make decrypt`.
3. Get the `macos_codesigning.keychain` file in the `secrets` directory and add
it to your local keychains with the Keychain Access app (File > Import
Items).
4. From that app delete the old certificate and add the new one (it's a `.cer`
file).
5. Test the certificate by right clicking and running both the general
evaluation and the codesigning one.
6. Update the identity in `MACOS_CODESIGN_IDENTITY` (You can find the identity
by doing `security find-identity -p codesigning $(PATH_TO_KEYCHAIN)/macos_codesigning.keychain` ).
7. You can also test it by running the codesign.sh script in this repo with
`./utilities/macos/codesign.sh --keychain ./secrets/macos_codesigning.keychain --identity $(NEW_IDENTITY) ./test`
with some executable.
8. Afterward reencrypt the keychain by running
`./cryptic-buildkite-plugin/bin/encrypt_file --private-key=$(INSERT_PRIVATE_KEY) --repo-key=$(INSERT_REPO_KEY) ./secrets/macos_codesigning.keychain`
9. Finally sign the repo with `make sign_treehashes`

The `security` cli app is also useful for debugging and managing keychains. You
can find more information about it by running `man security` in the terminal.
# macOS Developer ID certificate

The Developer ID Application private key lives in AWS KMS
(`alias/julia-macos-codesigning`); macOS codesigning happens via
`rcodesign` with the AWS KMS backend (see `utilities/macos/rcodesign/`).
The certificate itself is public and committed at
`utilities/macos/developer_id.pem`.

## Expired certificate

The main symptom will be failing upload jobs (signature validation errors
or Apple rejecting the notarization). To renew:

1. Generate a fresh CSR from the KMS key (no new key needed):
`ops/22_generate_macos_csr.sh`
2. Submit it at https://developer.apple.com/account/resources/certificates
(type: "Developer ID Application"), download the `.cer`.
3. Convert and commit:
`openssl x509 -inform DER -in developerID_application.cer -out utilities/macos/developer_id.pem`
4. Test locally (requires kms:Sign on the key):
`rcodesign sign --aws-kms-key alias/julia-macos-codesigning --aws-kms-certificate-file utilities/macos/developer_id.pem <some-binary>`

## New agreements

It is also possible that just a new agreement is needed. In that case, you will
see the following error message returned by the `codesign` command:
It is also possible that just a new agreement is needed. In that case, you
will see the following error message during notarization:

> HTTP status code: 403. A required agreement is missing or has expired. This
> request requires an in-effect agreement that has not been signed or has
> expired. Ensure your team has signed the necessary legal agreements and that
> they are not expired.
> HTTP status code: 403. A required agreement is missing or has expired.
> This request requires an in-effect agreement that has not been signed or
> has expired. Ensure your team has signed the necessary legal agreements
> and that they are not expired.

In this case, it is sufficient to visit `developer.apple.com` and logging in
In this case, it is sufficient to visit `developer.apple.com` and log in
using the Apple ID that is associated with the Apple Developer account. You
will be prompted to accept the new agreement. After that, the build should
succeed.

Note that currently, the Apple ID that's used to sign Julia binaries is owned
by JuliaHub, so you will need to get in touch with somebody from the
Note that currently, the Apple ID that's used to sign Julia binaries is
owned by JuliaHub, so you will need to get in touch with somebody from the
organization to accept the agreement.
36 changes: 0 additions & 36 deletions devdocs/sign.md

This file was deleted.

104 changes: 104 additions & 0 deletions ops/20_export_gpg_pubkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# This file is a part of Julia. License is MIT: https://julialang.org/license
"""Export the KMS-held release tarball signing key as an OpenPGP public key.

The signing key is generated inside KMS (alias/julia-tarball-signing,
created by ops/terraform) and never leaves it. This script fetches the
RSA public half via `aws kms get-public-key`, wraps it in an OpenPGP v4
public key packet + user ID, and obtains the positive self-certification
with a `kms:Sign` call -- producing an armored public key block that GPG
and friends accept. Commit the output (signing-pubkeys/tarball_signing.pub.asc)
and publish it as the Julia releases signing key; release signatures
made by utilities/kms_gpg_sign.py verify against it.

The OpenPGP fingerprint covers the key creation timestamp. It defaults to
the KMS key's own CreationDate (kms:DescribeKey) -- an immutable, already-
pinned value that also matches what kms_gpg_sign.py's --public-key-from-kms
signing path derives -- so re-running reproduces the identical key block.
--created overrides it only to reproduce a key minted under a different
timestamp; a changed timestamp would mint a "new" identity for the same key.

Usage:
20_export_gpg_pubkey.py [--created YYYY-MM-DD] [--kms-key-id ALIAS] [-o FILE]

For testing without KMS access, `--local-key key.pem` uses a local RSA
private key via openssl, exercising the identical construction path.

Requires AWS credentials with kms:GetPublicKey + kms:Sign on the key.
"""

import argparse
import os
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(SCRIPT_DIR, "..", "utilities"))

import kms_gpg_sign as K # noqa: E402

# The DER/SPKI parsing, kms:GetPublicKey fetch, and --created parsing live in
# kms_gpg_sign.py (shared with its --public-key-from-kms signing mode).

DEFAULT_KMS_KEY = "alias/julia-tarball-signing"
DEFAULT_UID = "Julia Release Signing Key <buildbot@julialang.org>"
DEFAULT_OUTPUT = os.path.join(SCRIPT_DIR, "..", "signing-pubkeys", "tarball_signing.pub.asc")


def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--created",
help="override the key creation time (unix timestamp or "
"YYYY-MM-DD UTC midnight); part of the key fingerprint. "
"Defaults to the KMS key's own CreationDate")
parser.add_argument("--uid", default=DEFAULT_UID,
help=f"user ID for the key (default: {DEFAULT_UID!r})")
parser.add_argument("-o", "--output", default=DEFAULT_OUTPUT,
help="output path for the armored public key block")
group = parser.add_mutually_exclusive_group()
group.add_argument("--kms-key-id", default=DEFAULT_KMS_KEY,
help=f"AWS KMS key ID/ARN/alias (default: {DEFAULT_KMS_KEY})")
group.add_argument("--local-key",
help="local RSA private key PEM (testing only)")
args = parser.parse_args()

if args.created:
timestamp = K.parse_created(args.created)
elif args.local_key:
# A local test key has no KMS CreationDate; pin a fixed epoch so the
# construction path stays deterministic.
timestamp = 0
else:
# Pin the fingerprint to the KMS key's immutable provisioning date,
# matching kms_gpg_sign.py's --public-key-from-kms signing path.
timestamp = K.kms_key_creation_date(args.kms_key_id)

if args.local_key:
spki = K.local_public_key(args.local_key)
signer = K.LocalOpensslSigner(args.local_key)
else:
spki = K.kms_public_key(args.kms_key_id)
signer = K.KmsSigner(args.kms_key_id)

n, e = K.rsa_components_from_spki(spki)
key_body = K.build_rsa_key_body(n, e, timestamp)
pubkey, cert = K.build_certificate(signer, key_body, args.uid, timestamp)
armored = K.armor(cert, "PUBLIC KEY BLOCK")

with open(args.output, "w") as f:
f.write(armored)

# Round-trip self-check: what we wrote parses back to the same key.
assert K.load_public_key(args.output).fingerprint == pubkey.fingerprint

print(f"Fingerprint: {pubkey.fingerprint.hex().upper()}")
print(f"User ID: {args.uid}")
print(f"Created: {timestamp}")
print(f"Written to: {args.output}")
print()
print("Commit this file, and publish it as the Julia releases signing key")
print("(it replaces the pre-migration juliareleases.asc).")


if __name__ == "__main__":
main()
Loading
Loading