Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9a47693
Deliver fallback installer and path hints for non-VS Code use
claude May 25, 2026
d01e4e8
Add read-only --doctor diagnostics to setup-cert.sh
claude May 25, 2026
49c94b9
Document non-VS Code use and ship a worked example
claude May 25, 2026
f331b7f
Move cert generator and exporter into the shared package
claude May 25, 2026
849fe7d
Move the platform trust-store layer into the shared package
claude May 25, 2026
a55cf43
Add ddc — a host CLI for generating, inspecting, and trusting dev certs
claude May 25, 2026
c184b70
Add hostCertGenerator setting; promote backends into shared
claude May 26, 2026
41ddc01
Drop aspire from public backend surface
claude May 26, 2026
4809f8e
Harden runProcess against Windows cwd-first executable lookup
claude May 26, 2026
30a2c4e
Document the ddc CLI in the manual-setup example and a new CLI README
claude May 26, 2026
08fb2a1
Native backend: skip the platform store entirely on --no-trust
claude May 26, 2026
a7227a7
ddc bundle: warn on cross-dir paths; ddc doctor: per-OS tool parity
claude May 26, 2026
15f33c5
Rename CLI binary ddc → dcdc; ready @devcontainer-dev-certs/cli for npm
claude May 26, 2026
aa1294c
CLI README: drop the ddc-collision backstory
claude May 26, 2026
5275537
Wire the CLI into the existing release pipeline
claude May 26, 2026
1d1f8a1
Add RELEASING.md covering the normal flow and the one-time CLI bootstrap
claude May 26, 2026
b990484
Fix dotnet backend: --no-password is PFX-invalid; bypass dotnet export
claude May 26, 2026
e3f90dd
Wire Linux NSS reporter through every trust-step entry point
claude May 26, 2026
27c965e
dcdc generate / bundle: trust symmetry + sibling-file probe parity
claude May 26, 2026
54406f7
setup-cert.sh: honor DEVCONTAINER_DEV_CERTS_*_DIR env vars
claude May 26, 2026
376e6c6
macStore.removeCertificates: untrust before delete, with cert file
claude May 26, 2026
a140e40
windowsStore: re-probe pwsh on each call if not yet found
claude May 26, 2026
4a04052
Backend cleanup: drop PFX reparse; collapse certProvider's double nat…
claude Jun 12, 2026
8a19d45
Move findSiblingKey to shared; consolidate CLI defaults
claude Jun 12, 2026
d3f5e07
dcdc doctor: probe dotnet once and run check groups concurrently
claude Jun 12, 2026
9707fb0
Extract stubPlatform into a per-workspace test helper
claude Jun 12, 2026
8e25995
Empirically verify macOS dotnet dev-certs disk-cache compatibility
claude Jun 12, 2026
9619a5e
Merge remote-tracking branch 'origin/main' into claude/devcontainer-n…
claude Jun 12, 2026
546b03e
Fix two CI failures from the main merge
claude Jun 12, 2026
d33c914
Fix two more CI failures: install.sh shellcheck + macOS keychain HOME
claude Jun 12, 2026
9b35bd9
setup-cert.sh: fix two shellcheck findings
claude Jun 12, 2026
09115d8
Accept the one legacy PBE algorithm aspnetcore writes on macOS
claude Jun 12, 2026
d40d0b7
Try both empty-password conventions in legacy 3DES decrypt
claude Jun 12, 2026
29d3204
Address four Copilot review findings
claude Jun 12, 2026
a6747fe
Bump Node floor from 20 to 22 (Node 20 went EOL April 2026)
claude Jun 12, 2026
7b92c6b
Bump CLI version to 1.3.2-pre to match the rest of the repo
claude Jun 12, 2026
66eb591
Bump all components to 1.4.0-pre.1
claude Jun 12, 2026
d4861f3
nativeBackend.test.ts: consolidate the two shared imports
claude Jun 12, 2026
33032ba
dcdc generate / --help: clearer reuse + interaction semantics
claude Jun 13, 2026
93311f8
Drop dcdc CLI from PR scope; keep shared layer + macOS support
claude Jun 13, 2026
17def4a
Restore cross-platform optional deps in package-lock.json
claude Jun 13, 2026
6bf93f8
Fix OID inspector in macOS PFX format compatibility test
claude Jun 13, 2026
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
56 changes: 53 additions & 3 deletions .github/workflows/build-extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
description: Use production build (minified, no sourcemaps)
type: boolean
default: false
upload-vsix:
description: Package and upload VSIX artifacts
upload-artifacts:
description: Package and upload release artifacts (VSIXes) for downstream publish jobs.
type: boolean
default: false

Expand Down Expand Up @@ -70,7 +70,7 @@ jobs:
run: npm run package -w src/vscode-workspace-extension

- name: Upload VSIX artifacts
if: inputs.upload-vsix
if: inputs.upload-artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vsix
Expand Down Expand Up @@ -107,6 +107,56 @@ jobs:
- name: Validate PFX with .NET
run: npm test -w src/vscode-ui-extension -- tests/dotnetPfx.integration.test.ts

macos-dotnet-dev-certs-cache:
name: macOS dotnet dev-certs disk-cache compatibility
runs-on: macos-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"

- name: Install dependencies
run: npm ci

- name: Build UI Extension
run: npm run ${{ inputs.production && 'build:prod' || 'build' }} -w src/vscode-ui-extension

- name: Set up .NET 10 SDK
# Test invokes `dotnet dev-certs https` to produce the actual
# on-disk cache file aspnetcore's MacOSCertificateManager
# writes. We need a real SDK on PATH; the exact major version
# is logged so we can correlate the result with SDK behavior
# if aspnetcore changes its export defaults.
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: "10.0.x"

- name: Unlock login keychain for dotnet dev-certs
# `dotnet dev-certs https` saves the generated cert into
# CurrentUser\My, which on macOS is the login keychain. GitHub
# Actions macos runners ship with the login keychain locked
# by default — without an unlock, the save call fails with
# "There was an error saving the HTTPS developer certificate
# to the current user personal certificate store." and the
# disk cache we're trying to test never gets written.
#
# The empty password is the documented GH Actions convention
# for the runner user's login keychain.
run: |
security unlock-keychain -p '' "$HOME/Library/Keychains/login.keychain-db"
security set-keychain-settings "$HOME/Library/Keychains/login.keychain-db"

- name: Validate dotnet dev-certs disk cache is loadable
# If this fails, the dotnet backend on macOS cannot discover
# the cert dotnet just created — platform-store discovery
# depends on `parsePfx` accepting whatever
# `certificate.Export(X509ContentType.Pfx)` writes on macOS.
# The same test pins the on-disk PBE algorithm OID so we get
# a loud signal the day aspnetcore switches to PBES2 and the
# legacy 3DES handler in `pkcs12LegacyPbe.ts` can be removed.
run: npm test -w src/vscode-ui-extension -- tests/dotnetMacosCache.integration.test.ts

feature:
name: Validate Devcontainer Feature
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ jobs:
uses: ./.github/workflows/build-extensions.yml
with:
production: true
upload-vsix: true
upload-artifacts: true
2 changes: 1 addition & 1 deletion .github/workflows/release-feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
uses: ./.github/workflows/build-extensions.yml
with:
production: true
upload-vsix: true
upload-artifacts: true

publish-feature:
name: Publish feature to GHCR
Expand Down
120 changes: 119 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Set under the feature entry in `devcontainer.json`:
| `syncUserCertificates` | `true` | Per-container opt-out for syncing certs configured in the host `devcontainerDevCerts.userCertificates` VS Code setting. |
| `syncContainerCert` | `false` | **Reverse sync (opt-in).** When the container itself already has a valid ASP.NET dev certificate (e.g. baked into the image with `dotnet dev-certs https`), push it to the host so the host trusts it instead of generating its own. Enabling this also implicitly overrides `generateDotNetCert` for this container — you don't need to set both. See "[Syncing a certificate from the container to the host](#syncing-a-certificate-from-the-container-to-the-host)". |
| `extraCertDestinations` | `""` | Comma-separated list of additional directories to write cert artifacts to. Each entry is `<abs-dir>[=<format>]` where `format` is `pem`, `key`, `pem-bundle`, `pfx`, or `all` (default). Every synced cert is written under the directory as `{name}.{pem,key,pfx}` (and/or `{name}-bundle.pem`). Example: `/etc/nginx/certs=pem,/var/myapp`. |
| `installFallbackTools` | `false` | Install the runtime prerequisites (`openssl`, `jq`) the fallback `devcontainer-dev-certs-install` script needs. The script itself is always delivered to `/usr/local/bin/` regardless of this option — set this to `true` only when you intend to invoke it manually (JetBrains / Vim / CLI users) and your base image does not already provide `openssl` and `jq`. See "[Manual / non-VS Code use](#manual--non-vs-code-use)". |

### Host VS Code settings

Expand Down Expand Up @@ -322,6 +323,123 @@ When this is on, non-local SAN entries are shown in the consent modal so you can

Pushes from a Dev Container without the matching feature option are ignored — the host setting on its own doesn't do anything until a container actively pushes. Host trust prompts fire per unique thumbprint, so opening multiple containers with different container-generated certs will accumulate trust prompts; this is intentional and is why the option isn't on by default.

## Manual / non-VS Code use

> [!NOTE]
> This is a **minimally supported** path. The first-class experience this project ships is the VS Code host + remote extension pair; everything below exists so a user without VS Code isn't left with no escape hatch, but it's hand-wired and there's no host-side automation for cert generation or OS trust. Treat it as a "you're on your own for the host half" workaround, not a primary supported scenario.

The companion-extension pattern is VS Code-specific, but the underlying container-side machinery isn't. JetBrains, Vim, raw CLI, and CI users can drive the in-container half of the trust state through a small shell tool the feature installs into the container. Host-side cert generation and OS trust remain the user's problem (typically `dotnet dev-certs https --trust`).

For an end-to-end walkthrough — generating the cert on the host, mounting it in, wiring `postStartCommand` — see [`examples/manual-setup/`](examples/manual-setup/). The summary here is the reference.

### The fallback installer

The feature delivers `devcontainer-dev-certs-install` to `/usr/local/bin/` during install. It writes to the same canonical paths the VS Code workspace extension uses — `~/.dotnet/corefx/cryptography/x509stores/{my,root}` and `~/.aspnet/dev-certs/trust/` (with c_rehash symlinks) — so Kestrel's `X509Store` fallback and OpenSSL clients discover certs installed this way exactly as they would extension-installed ones.

Three invocation forms:

```bash
# Single cert (legacy positional form)
devcontainer-dev-certs-install /path/to/cert.pfx /path/to/cert.pem <sha1-fingerprint>

# Multi-cert + extra destinations (preferred)
devcontainer-dev-certs-install --bundle-json /path/to/bundle.json

# Read-only diagnostics
devcontainer-dev-certs-install --doctor
```

The bundle form requires `openssl` and `jq`; the positional form needs only `openssl`. Set the feature option `installFallbackTools: true` to have the feature install them when they're missing from the base image.

### Bundle JSON

A bundle describes one or more certs and (optionally) extra destinations. The full schema lives at [`schema/bundle.schema.json`](schema/bundle.schema.json); reference it from your bundle file to get autocomplete and validation in any editor that honors JSON Schema:

```jsonc
{
"$schema": "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json",
"certs": [
{
"name": "aspnetcore-dev",
"kind": "dotnet-dev",
"thumbprint": "ABCDEF...",
"pfxPath": "/host-dev-certs/aspnetcore-dev.pfx",
"pemPath": "/host-dev-certs/aspnetcore-dev.pem",
"pemKeyPath": "/host-dev-certs/aspnetcore-dev.key",
"trustInContainer": true
}
],
"extraDestinations": [
{ "path": "/etc/nginx/certs", "format": "pem" }
]
}
```

Key fields:

| Field | Notes |
|-------|-------|
| `certs[].name` | Filename stem in extra destinations and (for user certs) in the OpenSSL trust dir. `[A-Za-z0-9._-]`, 1-64 chars. |
| `certs[].thumbprint` | SHA-1 fingerprint, hex, no separators. Used as the `.pfx` filename in the .NET store where Kestrel discovers it. |
| `certs[].pemPath` | Required. PEM-encoded cert. |
| `certs[].pfxPath` | PFX with private key. Required if you want Kestrel to serve TLS with this cert. |
| `certs[].pemKeyPath` | Private key in PEM form. Optional; needed only for `key` / `pem-bundle` extra destination formats. |
| `certs[].rootPfxPath` | Public-cert-only PFX for the .NET Root store. Synthesized via `openssl pkcs12 -nokeys` when omitted. |
| `certs[].trustInContainer` | Default `true`. Install into the Root store + OpenSSL trust dir, not just My. |
| `certs[].kind` | `dotnet-dev` uses the historic `aspnetcore-localhost-{thumbprint}.pem` filename; `user` (default) uses `{name}.pem`. |
| `extraDestinations[].path` | Absolute directory to write artifacts under. |
| `extraDestinations[].format` | `pem`, `key`, `pem-bundle`, `pfx`, or `all` (default). |

### Lifecycle: invoking the installer from `devcontainer.json`

Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host takes effect on the next container start without a rebuild:

```jsonc
{
"features": {
"ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1": {
"installFallbackTools": true
}
},
"mounts": [
"source=${localEnv:HOME}/.dev-certs,target=/host-dev-certs,type=bind,readonly"
],
"postStartCommand": "devcontainer-dev-certs-install --bundle-json /host-dev-certs/bundle.json || true"
}
```

The `|| true` keeps a missing or malformed bundle from blocking container startup.

### Path hints for integrations

The feature exports a handful of `DEVCONTAINER_DEV_CERTS_*` env vars so integration scripts don't have to hardcode the canonical paths. They're available in login shells (via `/etc/profile.d/`) and PAM-based sessions (via `/etc/environment`):

| Variable | Value |
|----------|-------|
| `DEVCONTAINER_DEV_CERTS_INSTALL_BIN` | `/usr/local/bin/devcontainer-dev-certs-install` |
| `DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR` | `~/.dotnet/corefx/cryptography/x509stores/my` |
| `DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR` | `~/.dotnet/corefx/cryptography/x509stores/root` |
| `DEVCONTAINER_DEV_CERTS_TRUST_DIR` | `~/.aspnet/dev-certs/trust` |
| `DEVCONTAINER_DEV_CERTS_GENERATE_DOTNET` | Mirror of the `generateDotNetCert` feature option. |
| `DEVCONTAINER_DEV_CERTS_SYNC_USER` | Mirror of the `syncUserCertificates` feature option. |
| `DEVCONTAINER_DEV_CERTS_SYNC_FROM_CONTAINER` | Mirror of the `syncContainerCert` feature option. |
| `DEVCONTAINER_DEV_CERTS_EXTRA_DESTINATIONS` | Mirror of the `extraCertDestinations` feature option. |

### Verifying with `--doctor`

`devcontainer-dev-certs-install --doctor` runs read-only checks across the trust infrastructure: prerequisite presence, trust-directory existence and writability, `SSL_CERT_DIR` state, the gating of `DOTNET_GENERATE_ASPNET_CERTIFICATE`, per-store cert listings (subject / `notAfter` / SHA-1), c_rehash symlink integrity, and expired-cert / multiple-dev-cert detection. Exits non-zero only when something is demonstrably broken, so it's safe to chain into CI:

```bash
devcontainer-dev-certs-install --bundle-json /host-dev-certs/bundle.json \
&& devcontainer-dev-certs-install --doctor
```

### What's still VS Code-only

- **Host-side cert generation and trust.** Use `dotnet dev-certs https --trust` (or your platform's equivalent) and export the PFX / PEM into your mounted host directory.
- **`defaultKestrelCertificate`.** Lives in a VS Code setting and is delivered via `EnvironmentVariableCollection`. Set `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` yourself in `devcontainer.json` `containerEnv` if you need this outside VS Code.
- **Reverse sync (`syncContainerCert`).** Needs a privileged host-side process with a consent UI; the script can't perform host trust.

## Development

### Prerequisites
Expand Down Expand Up @@ -351,7 +469,7 @@ This feature applies the workaround at the end of its own install script, so com
## Limitations

- **Auto-generated dev cert matches .NET's format only.** The `generateDotNetCert` flow produces a cert identical to `dotnet dev-certs https` (specific OID marker, subject, SAN entries). To sync differently-shaped certs (corporate CAs, custom wildcard certs, etc.), add them via the `devcontainerDevCerts.userCertificates` VS Code setting — they're copied as-is.
- **VS Code only.** The companion extension pattern relies on VS Code's cross-host command routing. Other editors (JetBrains, Vim, etc.) are not supported, though the devcontainer feature includes a `setup-cert.sh` fallback script (with a `--bundle-json` form for multi-cert bundles) for manual use.
- **Full automation is VS Code-only.** The host extension's certificate generation, OS trust, and cross-host routing rely on VS Code APIs. JetBrains, Vim, and CLI users can drive the same container-side trust state through the `devcontainer-dev-certs-install` fallback installer the feature ships at `/usr/local/bin/` — see "[Manual / non-VS Code use](#manual--non-vs-code-use)" — but cert generation and host trust are still on the user.
- **Host trust requires user interaction.** On Windows, trusting the auto-generated dev cert triggers a system dialog. On macOS, the keychain may prompt for a password. This only happens once and only for the .NET dev cert — user-managed certs are never added to the host OS trust store.

## Supported Platforms
Expand Down
24 changes: 24 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Releasing

This repository ships three components together on a single release trigger:

- The devcontainer feature (`ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs`)
- The host VS Code extension (`dnegstad.devcontainer-dev-certs-host`)
- The remote VS Code extension (`dnegstad.devcontainer-dev-certs-remote`)

All three share one repo-wide version. A release is one GitHub Release; everything downstream runs from `.github/workflows/release-feature.yml`.

## Normal release procedure

1. Verify every component is at the version you intend to release. `bump-version.yml` keeps them in lockstep automatically after the previous release, so this usually means "merge the open `chore/bump-X.Y.Z-pre` PR and then promote `X.Y.Z-pre` to `X.Y.Z`" via a separate bump PR.
2. Create a GitHub Release with tag `vX.Y.Z` (must match the component versions exactly — `validate-release` will fail the workflow otherwise).
3. Publish the GitHub Release. The `release: [released]` trigger starts the pipeline.

The workflow:

- Validates that the tag matches every component's `package.json` / `devcontainer-feature.json` version.
- Builds and tests everything via `build-extensions.yml` in production mode, producing VSIX files as workflow artifacts.
- Publishes the devcontainer feature to GHCR with build provenance.
- Attests VSIX provenance via `actions/attest-build-provenance` and attaches both VSIXes to the GitHub Release.

No manual publish commands. The GitHub Release is the trigger; CI does the rest.
Loading
Loading