diff --git a/UPGRADING.md b/UPGRADING.md index e16e91eb..1d2466f6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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) diff --git a/docs/guides/native-wallet.md b/docs/guides/native-wallet.md index 9ba0d4ee..d914b199 100644 --- a/docs/guides/native-wallet.md +++ b/docs/guides/native-wallet.md @@ -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 diff --git a/server/attester_chain_fuzz_test.go b/server/attester_chain_fuzz_test.go index 08fd9720..fafef382 100644 --- a/server/attester_chain_fuzz_test.go +++ b/server/attester_chain_fuzz_test.go @@ -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. @@ -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 { @@ -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 @@ -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}) @@ -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) } diff --git a/server/attester_leaf_policy.go b/server/attester_leaf_policy.go index bca2f98a..98e12d35 100644 --- a/server/attester_leaf_policy.go +++ b/server/attester_leaf_policy.go @@ -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 @@ -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:] { @@ -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 @@ -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. diff --git a/server/attester_leaf_policy_test.go b/server/attester_leaf_policy_test.go index 202eff0e..999b2b9b 100644 --- a/server/attester_leaf_policy_test.go +++ b/server/attester_leaf_policy_test.go @@ -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 ) @@ -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 @@ -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) @@ -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}, diff --git a/server/attester_trust.go b/server/attester_trust.go index f9d18860..5fe36226 100644 --- a/server/attester_trust.go +++ b/server/attester_trust.go @@ -137,9 +137,10 @@ func (r RegisteredAttesterKeys) validate() error { // rotated or revoked without redistributing anchors; // - come with intermediates only: "x5c" must not include the trust // anchor (HAIP 1.0), so an attestation is refused when any other -// "x5c" certificate is self-signed, or equals the anchor a verified -// chain ends at — even an anchor that is not self-signed, such as -// an intermediate configured as an anchor; +// "x5c" certificate is self-signed, or has the subject and public +// key of the anchor a verified chain ends at — even an anchor that +// is not self-signed, such as an intermediate configured as an +// anchor, and even a re-issued or cross-signed copy of it; // - permit digital signatures, if it restricts key usage at all; // - hold a key of the type the client's registered attestation // algorithm needs. @@ -147,8 +148,8 @@ func (r RegisteredAttesterKeys) validate() error { // No standard extended key usage exists for attestation signing, so // none is required, but LeafPolicy (DefaultAttesterLeafPolicy unless // set) refuses a signing certificate issued for another role — TLS -// server or client, OCSP responder, an ISO/IEC 18013-5 mdoc reader or -// document signer — or reachable only through a CA restricted to such +// server or client, OCSP responder, an ISO/IEC 18013-5 or 23220-4 +// mdoc role, document signing, SSH — or reachable only through a CA restricted to such // roles; only the chains it accepts are checked further. Revocation asks whether a certificate in the // verified chain below its anchor has been revoked, once the // attestation's PoP has verified; an anchor itself is withdrawn by an @@ -588,18 +589,22 @@ func verifyAttesterChain(ders [][]byte, roots *x509.CertPool, now time.Time) (*x } // checkX5CExcludesAnchors refuses x5c certificates (below the leaf) -// equal to the anchor any verified chain ends at — an anchor that is not -// self-signed, such as an intermediate configured as an anchor. It looks -// at every verified chain, not only those LeafPolicy or the issuer -// binding later keep, so an x5c that carries an anchor is refused -// whichever chain would otherwise be used. A pool's anchors can't be -// listed, so only those a chain reached are compared. +// for the anchor any verified chain ends at — an anchor that is not +// self-signed, such as an intermediate configured as an anchor. A +// certificate with the anchor's subject and public key is the anchor +// for path building, whatever else differs, so a copy re-issued by the +// anchor's own issuer or cross-signed by another CA is refused just as +// the configured certificate is. It looks at every verified chain, not +// only those LeafPolicy or the issuer binding later keep, so an x5c +// that carries an anchor is refused whichever chain would otherwise be +// used. A pool's anchors can't be listed, so only those a chain reached +// are compared. func checkX5CExcludesAnchors(extra []*x509.Certificate, chains [][]*x509.Certificate) error { for _, chain := range chains { // x509.Certificate.Verify never returns an empty chain. anchor := chain[len(chain)-1] for i, c := range extra { - if c.Equal(anchor) { + if bytes.Equal(c.RawSubject, anchor.RawSubject) && bytes.Equal(c.RawSubjectPublicKeyInfo, anchor.RawSubjectPublicKeyInfo) { return fmt.Errorf("attester x5c must not include the trust anchor (HAIP 1.0): x5c certificate %d is an anchor", i+1) } } diff --git a/server/attester_trust_test.go b/server/attester_trust_test.go index afa02e53..769dec2b 100644 --- a/server/attester_trust_test.go +++ b/server/attester_trust_test.go @@ -286,6 +286,36 @@ func TestX5CAttesterChainRefusesAnchorInX5C(t *testing.T) { } } +// TestX5CAttesterChainRefusesReissuedAnchorInX5C covers an anchor that +// is not self-signed (an intermediate trusted directly) reaching x5c as +// another certificate for the same subject and key: re-issued by the +// anchor's own parent, or cross-signed by an unrelated root. Either +// copy is the anchor, so HAIP 1.0 keeps it out of x5c just as it does +// the configured certificate itself. +func TestX5CAttesterChainRefusesReissuedAnchorInX5C(t *testing.T) { + root := newTestCert(t, "offline root", certOptions{isCA: true}) + anchor := newTestCert(t, "attester CA", certOptions{parent: &root, isCA: true}) + reissued := newTestCert(t, "attester CA", certOptions{parent: &root, isCA: true, key: anchor.key}) + otherRoot := newTestCert(t, "other root", certOptions{isCA: true}) + crossSigned := newTestCert(t, "attester CA", certOptions{parent: &otherRoot, isCA: true, key: anchor.key}) + otherKey := newTestCert(t, "attester CA", certOptions{parent: &root, isCA: true}) + leaf := newTestCert(t, "attester", certOptions{parent: &anchor}) + for name, tc := range map[string]struct { + x5c json.RawMessage + wantOK bool + }{ + "anchor itself": {x5cOf(leaf, anchor), false}, + "anchor re-issued by its parent": {x5cOf(leaf, reissued), false}, + "anchor cross-signed by other root": {x5cOf(leaf, crossSigned), false}, + "same subject, another key": {x5cOf(leaf, otherKey), true}, + "leaf alone": {x5cOf(leaf), true}, + } { + t.Run(name, func(t *testing.T) { + checkAttesterLeaf(t, leafPolicyHarness(t, anchor, nil), attesterLeaf{leaf, tc.x5c}, tc.wantOK) + }) + } +} + // TestX5CAttesterChainChecksValidityAtServerClock covers certificate // validity being judged at the server's injected Clock, not the wall // clock. The server's clock is set a month in the past, so each case