diff --git a/acme/challenge.go b/acme/challenge.go index f6067a17..cf658cf7 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -117,9 +117,17 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, case DEVICEATTEST01: return deviceAttest01Validate(ctx, ch, db, jwk, payload) case WIREOIDC01: - return wireOIDC01Validate(ctx, ch, db, jwk, payload) + wireDB, ok := db.(WireDB) + if !ok { + return NewErrorISE("db %T is not a WireDB", db) + } + return wireOIDC01Validate(ctx, ch, wireDB, jwk, payload) case WIREDPOP01: - return wireDPOP01Validate(ctx, ch, db, jwk, payload) + wireDB, ok := db.(WireDB) + if !ok { + return NewErrorISE("db %T is not a WireDB", db) + } + return wireDPOP01Validate(ctx, ch, wireDB, jwk, payload) default: return NewErrorISE("unexpected challenge type %q", ch.Type) } @@ -392,7 +400,7 @@ type wireOidcPayload struct { IDToken string `json:"id_token"` } -func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { +func wireOIDC01Validate(ctx context.Context, ch *Challenge, db WireDB, jwk *jose.JSONWebKey, payload []byte) error { prov, ok := ProvisionerFromContext(ctx) if !ok { return NewErrorISE("missing provisioner") @@ -522,7 +530,7 @@ type wireDpopPayload struct { AccessToken string `json:"access_token"` } -func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, accountJWK *jose.JSONWebKey, payload []byte) error { +func wireDPOP01Validate(ctx context.Context, ch *Challenge, db WireDB, accountJWK *jose.JSONWebKey, payload []byte) error { prov, ok := ProvisionerFromContext(ctx) if !ok { return NewErrorISE("missing provisioner") diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 5b97b3d2..9db45193 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -962,14 +962,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -984,6 +986,100 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= }, } }, + "fail/wire-oidc-01-no-wire-db": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{}, + err: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New("db *acme.MockDB is not a WireDB"), + }, + } + }, "ok/wire-dpop-01": func(t *testing.T) test { jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? @@ -1111,14 +1207,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -1134,6 +1232,141 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= }, } }, + "fail/wire-dpop-01-no-wire-db": func(t *testing.T) test { + jwk, _ := mustAccountAndKeyAuthorization(t, "token") + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuerexample.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{}, + err: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New("db *acme.MockDB is not a WireDB"), + }, + } + }, } for name, run := range tests { t.Run(name, func(t *testing.T) { diff --git a/acme/challenge_wire_test.go b/acme/challenge_wire_test.go index 1ac381ce..f591c613 100644 --- a/acme/challenge_wire_test.go +++ b/acme/challenge_wire_test.go @@ -29,7 +29,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= type test struct { ch *Challenge jwk *jose.JSONWebKey - db DB + db WireDB payload []byte ctx context.Context expectedErr *Error @@ -38,6 +38,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= "fail/no-provisioner": func(t *testing.T) test { return test{ ctx: context.Background(), + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -68,6 +69,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= })) return test{ ctx: ctx, + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -109,6 +111,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Status: StatusPending, Value: "1234", }, + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:malformed", Detail: "The request message was malformed", @@ -150,6 +153,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Status: StatusPending, Value: "1234", }, + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -203,6 +207,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Status: StatusPending, Value: string(valueBytes), }, + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -259,25 +264,27 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Status: StatusPending, Value: string(valueBytes), }, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error { - assert.Equal(t, "chID", ch.ID) - assert.Equal(t, "azID", ch.AuthorizationID) - assert.Equal(t, "accID", ch.AccountID) - assert.Equal(t, "token", ch.Token) - assert.Equal(t, ChallengeType("wire-dpop-01"), ch.Type) - assert.Equal(t, StatusInvalid, ch.Status) - assert.Equal(t, string(valueBytes), ch.Value) - if assert.NotNil(t, ch.Error) { - var k *Error // NOTE: the error is not returned up, but stored with the challenge instead - if errors.As(ch.Error, &k) { - assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) - assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) - assert.Equal(t, 400, k.Status) - assert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error()) + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error { + assert.Equal(t, "chID", ch.ID) + assert.Equal(t, "azID", ch.AuthorizationID) + assert.Equal(t, "accID", ch.AccountID) + assert.Equal(t, "token", ch.Token) + assert.Equal(t, ChallengeType("wire-dpop-01"), ch.Type) + assert.Equal(t, StatusInvalid, ch.Status) + assert.Equal(t, string(valueBytes), ch.Value) + if assert.NotNil(t, ch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(ch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error()) + } } - } - return nil + return nil + }, }, }, } @@ -410,14 +417,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return errors.New("fail") + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return errors.New("fail") + }, }, }, expectedErr: &Error{ @@ -556,14 +565,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -706,14 +717,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -856,14 +869,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -1012,14 +1027,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -1065,7 +1082,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= type test struct { ch *Challenge jwk *jose.JSONWebKey - db DB + db WireDB payload []byte srv *httptest.Server ctx context.Context @@ -1075,6 +1092,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= "fail/no-provisioner": func(t *testing.T) test { return test{ ctx: context.Background(), + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -1105,6 +1123,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= })) return test{ ctx: ctx, + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -1146,10 +1165,12 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Status: StatusPending, Value: "1234", }, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error { - assert.Equal(t, "chID", ch.ID) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error { + assert.Equal(t, "chID", ch.ID) + return nil + }, }, }, expectedErr: &Error{ @@ -1193,6 +1214,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= Status: StatusPending, Value: "1234", }, + db: &MockWireDB{}, expectedErr: &Error{ Type: "urn:ietf:params:acme:error:serverInternal", Detail: "The server experienced an internal error", @@ -1288,23 +1310,25 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusInvalid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - if assert.NotNil(t, updch.Error) { - var k *Error // NOTE: the error is not returned up, but stored with the challenge instead - if errors.As(updch.Error, &k) { - assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) - assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) - assert.Equal(t, 400, k.Status) - assert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error()) + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + if assert.NotNil(t, updch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(updch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error()) + } } - } - return nil + return nil + }, }, }, } @@ -1394,23 +1418,25 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusInvalid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - if assert.NotNil(t, updch.Error) { - var k *Error // NOTE: the error is not returned up, but stored with the challenge instead - if errors.As(updch.Error, &k) { - assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) - assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) - assert.Equal(t, 400, k.Status) - assert.Contains(t, k.Err.Error(), "keyAuthorization does not match") + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + if assert.NotNil(t, updch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(updch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Contains(t, k.Err.Error(), "keyAuthorization does not match") + } } - } - return nil + return nil + }, }, }, } @@ -1500,23 +1526,25 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusInvalid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - if assert.NotNil(t, updch.Error) { - var k *Error // NOTE: the error is not returned up, but stored with the challenge instead - if errors.As(updch.Error, &k) { - assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) - assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) - assert.Equal(t, 400, k.Status) - assert.Equal(t, `claims in OIDC ID token don't match: invalid 'preferred_username' "wireapp://%40bob@wire.com" after transformation`, k.Err.Error()) + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + if assert.NotNil(t, updch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(updch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Equal(t, `claims in OIDC ID token don't match: invalid 'preferred_username' "wireapp://%40bob@wire.com" after transformation`, k.Err.Error()) + } } - } - return nil + return nil + }, }, }, } @@ -1606,14 +1634,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return errors.New("fail") + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return errors.New("fail") + }, }, }, expectedErr: &Error{ @@ -1709,14 +1739,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -1816,14 +1848,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -1923,14 +1957,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) @@ -2036,14 +2072,16 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= payload: payload, ctx: ctx, jwk: jwk, - db: &MockDB{ - MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { - assert.Equal(t, "chID", updch.ID) - assert.Equal(t, "token", updch.Token) - assert.Equal(t, StatusValid, updch.Status) - assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) - assert.Equal(t, string(valueBytes), updch.Value) - return nil + db: &MockWireDB{ + MockDB: MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, }, MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { assert.Equal(t, "accID", accountID) diff --git a/acme/db.go b/acme/db.go index 4b1e8d33..bcbed417 100644 --- a/acme/db.go +++ b/acme/db.go @@ -54,8 +54,14 @@ type DB interface { GetOrder(ctx context.Context, id string) (*Order, error) GetOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) UpdateOrder(ctx context.Context, o *Order) error +} - // TODO(hs): put in a different interface +// WireDB is the interface used for operations on ACME Orders for Wire identifiers. This +// is not a general purpose interface, and it should only be used when Wire identifiers +// are enabled in the CA configuration. Currently it provides a runtime assertion only; +// not at compile time. +type WireDB interface { + DB GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]interface{}) error GetDpopToken(ctx context.Context, orderID string) (map[string]interface{}, error) @@ -126,14 +132,20 @@ type MockDB struct { MockGetOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error) MockUpdateOrder func(ctx context.Context, o *Order) error + MockRet1 interface{} + MockError error +} + +// MockWireDB is an implementation of the WireDB interface that should only be used as +// a mock in tests. It embeds the MockDB, as it is an extension of the existing database +// methods. +type MockWireDB struct { + MockDB MockGetAllOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error) MockGetDpopToken func(ctx context.Context, orderID string) (map[string]interface{}, error) MockCreateDpopToken func(ctx context.Context, orderID string, dpop map[string]interface{}) error MockGetOidcToken func(ctx context.Context, orderID string) (map[string]interface{}, error) MockCreateOidcToken func(ctx context.Context, orderID string, idToken map[string]interface{}) error - - MockRet1 interface{} - MockError error } // CreateAccount mock. @@ -407,7 +419,7 @@ func (m *MockDB) GetOrdersByAccountID(ctx context.Context, accID string) ([]stri } // GetAllOrdersByAccountID returns a list of any order IDs owned by the account. -func (m *MockDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) { +func (m *MockWireDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) { if m.MockGetAllOrdersByAccountID != nil { return m.MockGetAllOrdersByAccountID(ctx, accountID) } else if m.MockError != nil { @@ -417,7 +429,7 @@ func (m *MockDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) } // GetDpop retrieves a DPoP from the database. -func (m *MockDB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) { +func (m *MockWireDB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) { if m.MockGetDpopToken != nil { return m.MockGetDpopToken(ctx, orderID) } else if m.MockError != nil { @@ -427,7 +439,7 @@ func (m *MockDB) GetDpopToken(ctx context.Context, orderID string) (map[string]a } // CreateDpop creates DPoP resources and saves them to the DB. -func (m *MockDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error { +func (m *MockWireDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error { if m.MockCreateDpopToken != nil { return m.MockCreateDpopToken(ctx, orderID, dpop) } @@ -435,7 +447,7 @@ func (m *MockDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[s } // GetOidcToken retrieves an oidc token from the database. -func (m *MockDB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) { +func (m *MockWireDB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) { if m.MockGetOidcToken != nil { return m.MockGetOidcToken(ctx, orderID) } else if m.MockError != nil { @@ -445,7 +457,7 @@ func (m *MockDB) GetOidcToken(ctx context.Context, orderID string) (map[string]a } // CreateOidcToken creates oidc token resources and saves them to the DB. -func (m *MockDB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error { +func (m *MockWireDB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error { if m.MockCreateOidcToken != nil { return m.MockCreateOidcToken(ctx, orderID, idToken) } diff --git a/acme/order.go b/acme/order.go index cf2446fd..e941d587 100644 --- a/acme/order.go +++ b/acme/order.go @@ -208,6 +208,10 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques // Template data data := x509util.NewTemplateData() if o.containsWireIdentifiers() { + wireDB, ok := db.(WireDB) + if !ok { + return fmt.Errorf("db %T is not a WireDB", db) + } subject, err := createWireSubject(o, csr) if err != nil { return fmt.Errorf("failed creating Wire subject: %w", err) @@ -215,13 +219,13 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques data.SetSubject(subject) // Inject Wire's custom challenges into the template once they have been validated - dpop, err := db.GetDpopToken(ctx, o.ID) + dpop, err := wireDB.GetDpopToken(ctx, o.ID) if err != nil { return fmt.Errorf("failed getting Wire DPoP token: %w", err) } data.Set("Dpop", dpop) - oidc, err := db.GetOidcToken(ctx, o.ID) + oidc, err := wireDB.GetOidcToken(ctx, o.ID) if err != nil { return fmt.Errorf("failed getting Wire OIDC token: %w", err) }