Skip to content
Merged
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
74 changes: 74 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,80 @@ Most production-assurance changes only affect `Config.Assurance =
AssuranceProduction`. A development-assurance setup built on `memstore`
and `keys/ephemeral` needs only the steps not marked *production only*.

## v0.68.0

### `DefaultAttesterLeafPolicy` refuses more signing roles (server)

**Affects:** a server whose `Dependencies.AttesterTrust` is
`server.X5CAttesterChain` with the default `LeafPolicy` (nil or
`server.DefaultAttesterLeafPolicy{}`, including inside an
`AttesterLeafPolicyFunc`), when an attester's signing certificate
lists one of these extended key usages, or chains to the anchor only
through a CA whose extended key usage lists nothing but such roles:

- ISO/IEC 18013-5 mDL JWS signer (`1.0.18013.5.1.3`) or VICAL signer
(`1.0.18013.5.1.8`);
- ISO/IEC TS 23220-4 mdoc document signer (`1.0.23220.4.1.2`) or mdoc
reader authentication (`1.0.23220.4.1.6`);
- RFC 9336 document signing (`1.3.6.1.5.5.7.3.36`);
- RFC 6187 SSH client or server (`1.3.6.1.5.5.7.3.21`,
`1.3.6.1.5.5.7.3.22`).

**Why:** the default policy refuses certificates issued for another
role that signs things, so an anchor that also certifies those roles
can't turn their keys into attestation keys. v0.66.0 listed only the
mDL document signer and mdoc reader purposes of ISO/IEC 18013-5. It
missed the mDL JWS signer, which signs JWTs just as an attester does,
the same purposes on the ISO/IEC 23220-4 arc, and the IETF document
signing and SSH purposes.

**What fails:** nothing at startup. An attestation signed by such a
certificate is refused with `invalid_client`. Purposes the policy
doesn't list, such as an ecosystem's own Wallet Provider purpose, are
accepted as before.

**What to change:** issue the attester a dedicated attestation signing
certificate without another role's extended key usage. If the anchors
certify nothing but attesters, you may opt out with
`LeafPolicy: server.AnyAttesterLeaf{}`, or write a
`server.AttesterLeafPolicyFunc` that applies your own role rules
instead of calling `DefaultAttesterLeafPolicy`:

```go
deps.AttesterTrust = server.X5CAttesterChain{
// ...
LeafPolicy: server.AnyAttesterLeaf{},
}
```

### `X5CAttesterChain` refuses a re-issued or cross-signed anchor in `x5c` (server)

**Affects:** a server whose `Dependencies.AttesterTrust` is
`server.X5CAttesterChain`, when a wallet's attestation `x5c` carries,
after the signing certificate, a certificate with the same subject and
public key as an anchor a verified chain ends at, but not
byte-identical to it: a copy re-issued by the anchor's own issuer, or
cross-signed by another CA.

**Why:** HAIP 1.0 keeps the trust anchor out of `x5c`. v0.66.0 refused
an anchor in `x5c` only when its DER bytes matched the configured
anchor exactly, so another certificate for the same CA (same subject,
same key) got through. Path building treats such a certificate as the
anchor itself, so the check now compares subject and public key.

**What fails:** nothing at startup. Such an attestation is refused
with `invalid_client`. There is no option to keep the old behaviour.

**What to change:** have the wallet (or its Wallet Provider) send
`x5c` as the signing certificate followed by its intermediates only,
leaving out every certificate for a CA the server trusts as an
anchor, including cross-signed copies:

```go
// x5c: signing certificate, then intermediates, never the anchor.
x5c := [][]byte{leaf.Raw, intermediate.Raw}
```

## v0.66.0

### `X5CAttesterChain` refuses other roles' signing certificates (server)
Expand Down
8 changes: 5 additions & 3 deletions docs/guides/native-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@ own keys, by issuer (a `keys.AttesterKeySource` such as
`server.X5CAttesterChain` to verify the attestation's `x5c` chain. That
`x5c` carries the signing certificate and its intermediates only, as
HAIP 1.0 requires: an attestation whose `x5c` also includes a trust
anchor (any self-signed certificate, or a configured anchor) is
`invalid_client`. With
anchor (any self-signed certificate, or a certificate with a
configured anchor's subject and public key, such as a cross-signed
copy of it) is `invalid_client`. With
a trust list shared by several attesters, use its
`AttesterIssuerBoundToAnchor` binding, so each anchor vouches only for
the attesters it's bound to. Its `LeafPolicy`, by default
`server.DefaultAttesterLeafPolicy{}`, refuses a signing certificate
issued for another role (a TLS server or client, an OCSP responder, an
mdoc reader or document signer), or chaining only through a CA
ISO/IEC 18013-5 or 23220-4 mdoc reader, document signer, JWS or VICAL
signer, RFC 9336 document signing, or SSH), or chaining only through a CA
restricted to such roles, so an anchor that also certifies those can't
turn their keys into attestation keys. Wrap it in a
`server.AttesterLeafPolicyFunc` to also require your ecosystem's own
Expand Down
56 changes: 42 additions & 14 deletions server/attester_chain_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,35 @@ func (b *fuzzBytes) next() byte {
}

// fuzzEKUOIDs are the UnknownExtKeyUsage OIDs FuzzAttesterLeafPolicy
// draws from: both ISO/IEC 18013-5 roles, OIDs one arc either side of
// them, and an ecosystem's own purpose.
// draws from: the other signing roles the default refuses, OIDs one arc
// either side of some of them, and an ecosystem's own purpose.
var fuzzEKUOIDs = []asn1.ObjectIdentifier{
{1, 0, 18013, 5, 1, 2},
{1, 0, 18013, 5, 1, 3},
{1, 0, 18013, 5, 1, 6},
{1, 0, 18013, 5, 1, 8},
{1, 0, 23220, 4, 1, 2},
{1, 0, 23220, 4, 1, 6},
{1, 3, 6, 1, 5, 5, 7, 3, 21},
{1, 3, 6, 1, 5, 5, 7, 3, 22},
{1, 3, 6, 1, 5, 5, 7, 3, 36},
{1, 0, 18013, 5, 1},
{1, 0, 18013, 5, 1, 2, 1},
{1, 0, 18013, 5, 1, 7},
{1, 0, 23220, 4, 1, 3},
{1, 3, 6, 1, 5, 5, 7, 3, 37},
{1, 3, 6, 1, 4, 1, 99999, 1},
}

// fuzzAttesterCert is a certificate struct (never DER: the policy reads
// only parsed fields) with extended key usages drawn from b.
// only parsed fields) with extended key usages, and a subject and
// public key for the anchor rule, drawn from b.
func fuzzAttesterCert(b *fuzzBytes, i int) *x509.Certificate {
cert := &x509.Certificate{Subject: pkix.Name{CommonName: string(rune('a' + i))}, Raw: []byte{b.next() % 4}}
cert := &x509.Certificate{
Subject: pkix.Name{CommonName: string(rune('a' + i))},
RawSubject: []byte{1 + b.next()%3},
RawSubjectPublicKeyInfo: []byte{1 + b.next()%3},
}
for range b.next() % 4 {
// 0 is anyExtendedKeyUsage; values past crypto/x509's named ones
// are refused like any other non-Any purpose.
Expand All @@ -53,15 +67,21 @@ func fuzzAttesterCert(b *fuzzBytes, i int) *x509.Certificate {
return cert
}

// isISO18013RoleOracle is isISO18013Role written out by dotted form.
func isISO18013RoleOracle(oid asn1.ObjectIdentifier) bool {
s := oid.String()
return s == "1.0.18013.5.1.2" || s == "1.0.18013.5.1.6"
// isOtherSigningRoleOracle is isOtherSigningRole written out by dotted
// form, from the documented list.
func isOtherSigningRoleOracle(oid asn1.ObjectIdentifier) bool {
switch oid.String() {
case "1.0.18013.5.1.2", "1.0.18013.5.1.3", "1.0.18013.5.1.6", "1.0.18013.5.1.8",
"1.0.23220.4.1.2", "1.0.23220.4.1.6",
"1.3.6.1.5.5.7.3.36", "1.3.6.1.5.5.7.3.21", "1.3.6.1.5.5.7.3.22":
return true
}
return false
}

// attesterChainRefusedOracle is DefaultAttesterLeafPolicy's documented
// rule, independently: refuse an empty chain, a signing certificate
// listing any purpose but anyExtendedKeyUsage or an ISO/IEC 18013-5
// listing any purpose but anyExtendedKeyUsage or another signing
// role, and a chain through a CA whose extended key usage is present
// and lists neither anyExtendedKeyUsage nor a purpose other than those.
func attesterChainRefusedOracle(chain []*x509.Certificate) bool {
Expand All @@ -70,12 +90,12 @@ func attesterChainRefusedOracle(chain []*x509.Certificate) bool {
}
leaf := chain[0]
if slices.ContainsFunc(leaf.ExtKeyUsage, func(e x509.ExtKeyUsage) bool { return e != x509.ExtKeyUsageAny }) ||
slices.ContainsFunc(leaf.UnknownExtKeyUsage, isISO18013RoleOracle) {
slices.ContainsFunc(leaf.UnknownExtKeyUsage, isOtherSigningRoleOracle) {
return true
}
for _, ca := range chain[1:] {
listsAny := slices.Contains(ca.ExtKeyUsage, x509.ExtKeyUsageAny)
listsOwnPurpose := slices.ContainsFunc(ca.UnknownExtKeyUsage, func(o asn1.ObjectIdentifier) bool { return !isISO18013RoleOracle(o) })
listsOwnPurpose := slices.ContainsFunc(ca.UnknownExtKeyUsage, func(o asn1.ObjectIdentifier) bool { return !isOtherSigningRoleOracle(o) })
listsSome := len(ca.ExtKeyUsage)+len(ca.UnknownExtKeyUsage) > 0
if listsSome && !listsAny && !listsOwnPurpose {
return true
Expand All @@ -94,7 +114,8 @@ func attesterChainRefusedOracle(chain []*x509.Certificate) bool {
// - approvedAttesterChains keeps exactly the chains the policy
// accepts, in order, and fails closed when it keeps none;
// - checkX5CExcludesAnchors refuses exactly when an x5c certificate
// below the leaf equals the last certificate of some chain.
// below the leaf has the subject and public key of the last
// certificate of some chain.
func FuzzAttesterLeafPolicy(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{1, 0, 0, 0})
Expand Down Expand Up @@ -155,10 +176,17 @@ func checkX5CAnchorRule(t *testing.T, extra []*x509.Certificate, chains [][]*x50
nonEmpty = append(nonEmpty, c)
}
}
want := false
// A certificate is identified, for path building, by its subject
// and public key together.
type identity struct{ subject, key string }
anchors := map[identity]bool{}
for _, c := range nonEmpty {
want = want || slices.ContainsFunc(extra, func(e *x509.Certificate) bool { return bytes.Equal(e.Raw, c[len(c)-1].Raw) })
a := c[len(c)-1]
anchors[identity{string(a.RawSubject), string(a.RawSubjectPublicKeyInfo)}] = true
}
want := slices.ContainsFunc(extra, func(e *x509.Certificate) bool {
return anchors[identity{string(e.RawSubject), string(e.RawSubjectPublicKeyInfo)}]
})
if err := checkX5CExcludesAnchors(extra, nonEmpty); (err != nil) != want {
t.Fatalf("checkX5CExcludesAnchors = %v, want refusal %v", err, want)
}
Expand Down
61 changes: 47 additions & 14 deletions server/attester_leaf_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,16 @@ func (f AttesterLeafPolicyFunc) CheckAttesterChain(chain []*x509.Certificate) er
// server-gated-crypto and code-signing purposes — even alongside
// other purposes, since the certificate then also serves that role
// and its key may be held where that role is;
// - a signing certificate whose extended key usage lists an
// ISO/IEC 18013-5 role: mDL document signer (1.0.18013.5.1.2) or
// mdoc reader authentication (1.0.18013.5.1.6);
// - a signing certificate whose extended key usage lists one of
// these other signing roles, which crypto/x509 doesn't name:
// ISO/IEC 18013-5 mDL document signer (1.0.18013.5.1.2), mDL JWS
// signer for server retrieval (1.0.18013.5.1.3), mdoc reader
// authentication (1.0.18013.5.1.6) or VICAL signer
// (1.0.18013.5.1.8); ISO/IEC TS 23220-4 mdoc document signer
// (1.0.23220.4.1.2) or mdoc reader authentication
// (1.0.23220.4.1.6); RFC 9336 document signing
// (id-kp-documentSigning); or RFC 6187 SSH client or server
// authentication (id-kp-secureShellClient, id-kp-secureShellServer);
// - a chain through a CA certificate (an intermediate or the anchor)
// whose extended key usage lists only such purposes, such as a CA
// restricted to issuing TLS certificates, so a certificate under it
Expand All @@ -92,8 +99,8 @@ func (DefaultAttesterLeafPolicy) CheckAttesterChain(chain []*x509.Certificate) e
}
}
for _, oid := range leaf.UnknownExtKeyUsage {
if isISO18013Role(oid) {
return fmt.Errorf("attester certificate's extended key usage lists an ISO/IEC 18013-5 role (%s)", oid)
if name, ok := otherSigningRole(oid); ok {
return fmt.Errorf("attester certificate's extended key usage lists another role (%s, %s)", name, oid)
}
}
for _, ca := range chain[1:] {
Expand All @@ -118,15 +125,41 @@ func (AnyAttesterLeaf) CheckAttesterChain([]*x509.Certificate) error {
return nil
}

// ISO/IEC 18013-5 Annex B extended key usages, which crypto/x509 leaves
// in UnknownExtKeyUsage.
var (
oidMDLDocumentSigner = asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 2}
oidMdocReaderAuth = asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 6}
)
// otherSigningRoles are the extended key usages, left in
// UnknownExtKeyUsage by crypto/x509, of roles whose keys sign something
// other than attestations.
var otherSigningRoles = []struct {
oid asn1.ObjectIdentifier
name string
}{
// ISO/IEC 18013-5 Annex B (id-mdl-kp) and Annex C (VICAL).
{asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 2}, "mDL document signer"},
{asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 3}, "mDL JWS signer"},
{asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 6}, "mdoc reader authentication"},
{asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 8}, "mDL VICAL signer"},
// ISO/IEC TS 23220-4 Annex B, the same purposes on the mdoc arc.
{asn1.ObjectIdentifier{1, 0, 23220, 4, 1, 2}, "mdoc document signer"},
{asn1.ObjectIdentifier{1, 0, 23220, 4, 1, 6}, "mdoc reader authentication"},
// RFC 9336 and RFC 6187, under id-kp (1.3.6.1.5.5.7.3).
{asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 36}, "document signing"},
{asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 21}, "SSH client"},
{asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 22}, "SSH server"},
}

// otherSigningRole names the role oid is the extended key usage of, if
// it is one of otherSigningRoles.
func otherSigningRole(oid asn1.ObjectIdentifier) (string, bool) {
for _, r := range otherSigningRoles {
if oid.Equal(r.oid) {
return r.name, true
}
}
return "", false
}

func isISO18013Role(oid asn1.ObjectIdentifier) bool {
return oid.Equal(oidMDLDocumentSigner) || oid.Equal(oidMdocReaderAuth)
func isOtherSigningRole(oid asn1.ObjectIdentifier) bool {
_, ok := otherSigningRole(oid)
return ok
}

// restrictedToOtherRoles reports whether ca's extended key usage is
Expand All @@ -137,7 +170,7 @@ func restrictedToOtherRoles(ca *x509.Certificate) bool {
return false
}
return !slices.Contains(ca.ExtKeyUsage, x509.ExtKeyUsageAny) &&
!slices.ContainsFunc(ca.UnknownExtKeyUsage, func(oid asn1.ObjectIdentifier) bool { return !isISO18013Role(oid) })
!slices.ContainsFunc(ca.UnknownExtKeyUsage, func(oid asn1.ObjectIdentifier) bool { return !isOtherSigningRole(oid) })
}

// extKeyUsageName names eku for an error message.
Expand Down
15 changes: 13 additions & 2 deletions server/attester_leaf_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
var (
oidMDLDocumentSigner = asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 2}
oidMdocReaderAuth = asn1.ObjectIdentifier{1, 0, 18013, 5, 1, 6}
oidDocumentSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 36}
oidWalletProvider = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 1} // a private purpose
)

Expand Down Expand Up @@ -42,11 +43,19 @@ func roleLeaves(t *testing.T, root testCert) (refused, accepted map[string]attes
"OCSP responder": leaf("ocsp", certOptions{extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}}),
"mdoc reader": leaf("mdoc reader", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidMdocReaderAuth}}),
"mDL document signer": leaf("mdl ds", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidMDLDocumentSigner}}),
"mDL JWS signer": leaf("mdl jws", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 0, 18013, 5, 1, 3}}}),
"mDL VICAL signer": leaf("mdl vical", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 0, 18013, 5, 1, 8}}}),
"mdoc document signer": leaf("mdoc ds", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 0, 23220, 4, 1, 2}}}),
"mdoc reader (ISO/IEC 23220-4)": leaf("mdoc reader 23220", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 0, 23220, 4, 1, 6}}}),
"document signing": leaf("document signing", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidDocumentSigning}}),
"SSH client": leaf("ssh client", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 3, 6, 1, 5, 5, 7, 3, 21}}}),
"SSH server": leaf("ssh server", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 3, 6, 1, 5, 5, 7, 3, 22}}}),
"only path through a TLS-only CA": {underTLSCA, x5cOf(underTLSCA, tlsCA)},
}
accepted = map[string]attesterLeaf{
"no extended key usage": leaf("plain", certOptions{}),
"private purpose only": leaf("private", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidWalletProvider}}),
"unlisted ISO arc": leaf("unlisted", certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 0, 18013, 5, 1, 7}}}),
"anyExtendedKeyUsage": leaf("any", certOptions{extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}}),
}
return refused, accepted
Expand Down Expand Up @@ -84,8 +93,9 @@ func checkAttesterLeaf(t *testing.T, h harness, l attesterLeaf, wantOK bool) {

// TestX5CAttesterChainDefaultLeafPolicy covers the default role policy:
// with LeafPolicy nil, a certificate issued for another role (TLS, OCSP,
// an ISO/IEC 18013-5 role, or under a TLS-only CA) can't sign an
// attestation, while a certificate naming no other role can.
// an ISO/IEC 18013-5 or 23220-4 role, document signing, SSH, or under
// a TLS-only CA) can't sign an attestation, while a certificate naming
// no other role, or only a purpose the policy doesn't list, can.
func TestX5CAttesterChainDefaultLeafPolicy(t *testing.T) {
root := newTestCert(t, "ecosystem CA", certOptions{isCA: true})
refused, accepted := roleLeaves(t, root)
Expand Down Expand Up @@ -221,6 +231,7 @@ func TestDefaultAttesterLeafPolicyRoles(t *testing.T) {
"anyExtendedKeyUsage and TLS leaf": {leafUnder(plainCA, certOptions{extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny, x509.ExtKeyUsageServerAuth}}), false},
"private purpose and mdoc reader": {leafUnder(plainCA, certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidWalletProvider, oidMdocReaderAuth}}), false},
"CA restricted to mDL signing": {leafUnder(ca(certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidMDLDocumentSigner}}), certOptions{}), false},
"CA restricted to document signing": {leafUnder(ca(certOptions{unknownExtKeyUsage: []asn1.ObjectIdentifier{oidDocumentSigning}}), certOptions{}), false},
"CA restricted to TLS and OCSP": {leafUnder(ca(certOptions{extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning}}), certOptions{}), false},
"anchor restricted to TLS": {[]*x509.Certificate{newTestCert(t, "leaf", certOptions{parent: &root}).cert, ca(certOptions{extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}).cert}, false},
"CA with TLS and a private purpose": {leafUnder(ca(certOptions{extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, unknownExtKeyUsage: []asn1.ObjectIdentifier{oidWalletProvider}}), certOptions{}), true},
Expand Down
Loading
Loading