Skip to content

Commit cf1118b

Browse files
authored
feat(authz): work out the access level automatically, retire opext (#214)
CreateOperatorCredentialRequest already builds the extfile section a CA operator needs from internal/authz's codec (MarshalLevelExtension via operatorca.ExtfileSection), and first run's RegisterOperatorCA preview does the same for the first admin certificate. cmd/opext's manual byte-crunching step was dead weight left over from before that flow existed, so it's gone, along with the docs that told readers to run it and copy bytes out of a table. A handful of test fixtures still hand-rolled the extension value with asn1.Marshal(level.Token()) instead of calling authz.MarshalLevelValue; they now point at the same codec as everything else. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> Co-authored-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
1 parent a2dd853 commit cf1118b

9 files changed

Lines changed: 40 additions & 155 deletions

File tree

‎cmd/opext/main.go‎

Lines changed: 0 additions & 55 deletions
This file was deleted.

‎cmd/opext/main_test.go‎

Lines changed: 0 additions & 59 deletions
This file was deleted.

‎docs/deploying-standalone.md‎

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -127,40 +127,44 @@ operator CA (as above) rather than reusing the fleet intermediate, so the set of
127127
certs that can open a TLS session to the management plane stays exactly the set
128128
you minted for operators.
129129

130-
### The level extension in OpenSSL form
130+
### The level extension comes from the manager, not by hand
131131

132-
`go run ./cmd/opext` prints the level extension as a byte sequence
133-
(`[19, 5, 97, 100, 109, 105, 110]`). An OpenSSL operator CA needs the same DER
134-
in `-extfile` form. These are identical bytes — `0x13` is ASN.1
135-
`PrintableString`, `0x05` its length:
132+
Every operator certificate carries its access level in the extension
133+
`1.3.6.1.4.1.59999.1.1`. Nobody hand-builds that DER or copies it from a table —
134+
the manager derives it from the one codec (`internal/authz.MarshalLevelValue`)
135+
and hands you a ready-to-use OpenSSL section:
136136

137-
```ini
138-
# op-admin.ext
139-
extendedKeyUsage = clientAuth
140-
keyUsage = critical, digitalSignature
141-
basicConstraints = critical, CA:FALSE
142-
1.3.6.1.4.1.59999.1.1 = DER:13:05:61:64:6D:69:6E
143-
```
137+
- **The very first admin certificate**, before any admin exists to call the
138+
API, comes from first run: `RegisterOperatorCA`'s preview response includes
139+
`admin_extfile`, the `[ op_admin ]` section below. See
140+
[first-run.md](first-run.md).
141+
- **Every credential after that** comes from `CreateOperatorCredentialRequest`,
142+
whose response includes `extfile_section` for the level requested (`viewer`,
143+
`operator` or `admin`) and the `openssl ca` command that signs it with that
144+
section. See [operator-credentials.md](operator-credentials.md).
144145

145-
`basicConstraints` matters: without it `openssl x509 -req -extfile` emits a v3
146-
certificate carrying **no** basic constraints extension at all, which is not what the
147-
cryptos profile path produces for a leaf and not what you want an operator credential to
148-
look like. Pin it to `CA:FALSE` explicitly.
146+
Save the section the API gave you, for example as `op-admin.ext`:
149147

150-
| Level | DER | `opext` bytes |
151-
| --- | --- | --- |
152-
| `viewer` | `DER:13:06:76:69:65:77:65:72` | `[19, 6, 118, 105, 101, 119, 101, 114]` |
153-
| `operator` | `DER:13:08:6F:70:65:72:61:74:6F:72` | `[19, 8, 111, 112, 101, 114, 97, 116, 111, 114]` |
154-
| `admin` | `DER:13:05:61:64:6D:69:6E` | `[19, 5, 97, 100, 109, 105, 110]` |
148+
```ini
149+
# op-admin.ext (from admin_extfile / extfile_section)
150+
[ op_admin ]
151+
basicConstraints = critical, CA:FALSE
152+
keyUsage = critical, digitalSignature
153+
extendedKeyUsage = clientAuth
154+
subjectKeyIdentifier = hash
155+
authorityKeyIdentifier = keyid
156+
1.3.6.1.4.1.59999.1.1 = DER:13:05:61:64:6D:69:6E
157+
```
155158

156-
Mint and verify an operator certificate:
159+
Then mint and verify the operator certificate:
157160

158161
```sh
159162
openssl ecparam -name secp384r1 -genkey -noout -out operator-admin.key
160163
chmod 600 operator-admin.key
161164
openssl req -new -key operator-admin.key -subj "/CN=you@acme.example" -out operator-admin.csr
162165
openssl x509 -req -in operator-admin.csr -CA operator-ca.crt -CAkey operator-ca.key \
163-
-CAcreateserial -days 365 -sha384 -extfile op-admin.ext -out operator-admin.crt
166+
-CAcreateserial -days 365 -sha384 -extfile op-admin.ext -extensions op_admin \
167+
-out operator-admin.crt
164168

165169
openssl x509 -in operator-admin.crt -text -noout | grep -A2 '59999.1.1' # ..admin
166170
openssl verify -CAfile operator-ca.crt operator-admin.crt # OK

‎docs/operator-credentials.md‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,12 @@ people.
205205
206206
An operator certificate carries its access level in the extension
207207
`1.3.6.1.4.1.59999.1.1` (`authz.AccessLevelOID`): the DER of an ASN.1
208-
PrintableString of `viewer`, `operator` or `admin`.
209-
210-
| Level | OpenSSL `DER:` value | `opext` bytes |
211-
| --- | --- | --- |
212-
| `viewer` | `DER:13:06:76:69:65:77:65:72` | `[19, 6, 118, 105, 101, 119, 101, 114]` |
213-
| `operator` | `DER:13:08:6F:70:65:72:61:74:6F:72` | `[19, 8, 111, 112, 101, 114, 97, 116, 111, 114]` |
214-
| `admin` | `DER:13:05:61:64:6D:69:6E` | `[19, 5, 97, 100, 109, 105, 110]` |
215-
216-
`go run ./cmd/opext -level <level>` prints the byte form from the same encoding
217-
the manager reads.
208+
PrintableString of `viewer`, `operator` or `admin`. `internal/authz.MarshalLevelValue`
209+
is the single place that encodes it. `CreateOperatorCredentialRequest`'s
210+
`extfile_section` above, and first run's `admin_extfile`
211+
([first-run.md](first-run.md)), both come from that same codec
212+
(`internal/operatorca.ExtfileSection`), so nothing hand-builds the DER bytes or
213+
copies them from a table.
218214
219215
> [!NOTE]
220216
> `1.3.6.1.4.1.59999.1.1` is a placeholder arc. An IANA Private Enterprise

‎internal/authz/level.go‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,9 @@ func MarshalLevelValue(l Level) ([]byte, error) {
8484

8585
// MarshalLevelExtension returns the dotted OID and DER value of the
8686
// access-level extension for the given level token. It is the single encoding
87-
// used by both cmd/opext (which prints the value for a profile's
88-
// extra_extensions) and the operator-CA profile setup that stamps an
89-
// operator-<level> profile with the level extension. It errors if level is not
90-
// one of viewer|operator|admin.
87+
// used by the operator-CA profile setup (internal/operatorca.ExtfileSection)
88+
// that builds the op_<level> extfile section a CA operator signs an operator
89+
// credential with. It errors if level is not one of viewer|operator|admin.
9190
func MarshalLevelExtension(level string) (oid string, der []byte, err error) {
9291
l, err := LevelFromToken(level)
9392
if err != nil {

‎internal/fleet/mcpkeys_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import (
4545
// self-signed operator certificate at level with the given serial.
4646
func certCtx(t *testing.T, serial int64, level authz.Level) context.Context {
4747
t.Helper()
48-
value, _ := asn1.Marshal(level.Token())
48+
value, _ := authz.MarshalLevelValue(level)
4949
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
5050
tmpl := &x509.Certificate{
5151
SerialNumber: big.NewInt(serial),

‎internal/mcpauth/testcerts_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (ca testCA) pool() *x509.CertPool {
7272
// notBefore to notAfter.
7373
func (ca testCA) operatorCert(t *testing.T, serial int64, level authz.Level, notBefore, notAfter time.Time) *x509.Certificate {
7474
t.Helper()
75-
value, err := asn1.Marshal(level.Token())
75+
value, err := authz.MarshalLevelValue(level)
7676
if err != nil {
7777
t.Fatal(err)
7878
}

‎internal/mcpserver/harness_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func (noneRevoked) CheckMCP(string, string) error { return nil }
231231
// key mints a key for a fresh operator certificate at level with ceiling.
232232
func (h *harness) key(level authz.Level, ceiling string) string {
233233
h.t.Helper()
234-
value, _ := asn1.Marshal(level.Token())
234+
value, _ := authz.MarshalLevelValue(level)
235235
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
236236
serial, _ := rand.Int(rand.Reader, big.NewInt(1<<62))
237237
tmpl := &x509.Certificate{

‎internal/oauth/oauth_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func newFixture(t *testing.T) *fixture {
7676
}
7777

7878
func (f *fixture) operatorCert(serial int64, level authz.Level) *x509.Certificate {
79-
value, _ := asn1.Marshal(level.Token())
79+
value, _ := authz.MarshalLevelValue(level)
8080
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
8181
tmpl := &x509.Certificate{
8282
SerialNumber: big.NewInt(serial), Subject: pkix.Name{CommonName: "operator@example.org"},

0 commit comments

Comments
 (0)