Compare commits
141 Commits
master
...
v0.25.3-rc
Author | SHA1 | Date |
---|---|---|
|
675e418fc3 | 5 months ago |
|
502334fd82 | 5 months ago |
|
a38132aa58 | 5 months ago |
|
93ba1654ea | 5 months ago |
|
9eed61a9c5 | 5 months ago |
|
b8eb559ee9 | 5 months ago |
|
a3de984ee3 | 5 months ago |
|
7e6356ece2 | 5 months ago |
|
51d1270541 | 5 months ago |
|
19dbd02451 | 5 months ago |
|
2f3819aa4e | 5 months ago |
|
36e14de882 | 5 months ago |
|
f150a4f850 | 5 months ago |
|
f221232a80 | 5 months ago |
|
b9254744a2 | 5 months ago |
|
0a7fe6ebe9 | 5 months ago |
|
0f0f060149 | 5 months ago |
|
17578b57f2 | 5 months ago |
|
31bba6fbd8 | 5 months ago |
|
33be5523da | 5 months ago |
|
7680da7c57 | 5 months ago |
|
99934ec9a3 | 5 months ago |
|
37106a438a | 5 months ago |
|
7520736f5b | 5 months ago |
|
a24b2a5c84 | 5 months ago |
|
8f129a6ced | 5 months ago |
|
d84abac4df | 5 months ago |
|
a2304c8498 | 5 months ago |
|
c46434f6e0 | 5 months ago |
|
bca179d611 | 5 months ago |
|
2efd1f682d | 5 months ago |
|
7d5a79190d | 5 months ago |
|
768a08965d | 5 months ago |
|
29202eff26 | 5 months ago |
|
d5b0d92bce | 5 months ago |
|
0ad381b092 | 5 months ago |
|
2c27e865cb | 5 months ago |
|
9bb1b24bf1 | 5 months ago |
|
3f37feae78 | 5 months ago |
|
c8160caacd | 5 months ago |
|
24795720e1 | 5 months ago |
|
79739e5073 | 5 months ago |
|
7eacb68361 | 5 months ago |
|
44721a7d58 | 5 months ago |
|
348363abce | 5 months ago |
|
1bf807add3 | 5 months ago |
|
1f5f756fce | 5 months ago |
|
6ef64b6ed6 | 5 months ago |
|
b6fc0005d5 | 5 months ago |
|
b964c97750 | 5 months ago |
|
acad227b25 | 5 months ago |
|
cd9480ab14 | 5 months ago |
|
897688a831 | 5 months ago |
|
ca8855767d | 5 months ago |
|
70a2f431fa | 5 months ago |
|
de25740567 | 5 months ago |
|
c7892e9cd3 | 5 months ago |
|
a423151207 | 5 months ago |
|
ffd887f8cc | 5 months ago |
|
8997ce1a1e | 5 months ago |
|
bf8c17e3ec | 5 months ago |
|
033aef9f9d | 5 months ago |
|
6a98fea1f3 | 5 months ago |
|
8faf26c593 | 5 months ago |
|
bf5f1201ea | 5 months ago |
|
e2a2e00526 | 5 months ago |
|
29fa6621b1 | 5 months ago |
|
7a464cdb17 | 5 months ago |
|
776a839a42 | 5 months ago |
|
f5a2f436df | 5 months ago |
|
eb9893bd21 | 5 months ago |
|
40668ae09e | 5 months ago |
|
01169b2483 | 5 months ago |
|
85309bb8ec | 5 months ago |
|
fdea5e7db3 | 5 months ago |
|
c1a7acc306 | 5 months ago |
|
84e9682476 | 5 months ago |
|
90b5347887 | 5 months ago |
|
39bf889925 | 5 months ago |
|
d6ceebba94 | 5 months ago |
|
6ffd913e28 | 5 months ago |
|
2be77385f6 | 5 months ago |
|
ff07fdc0fd | 5 months ago |
|
13df461e97 | 5 months ago |
|
83f76433a8 | 5 months ago |
|
8fd0192da3 | 5 months ago |
|
4d028f7813 | 5 months ago |
|
ed2bce9a3c | 5 months ago |
|
5fdf036a4d | 5 months ago |
|
9d5c974f44 | 5 months ago |
|
1b32957ff6 | 5 months ago |
|
ab9e1ddb28 | 5 months ago |
|
7b5740153d | 5 months ago |
|
f5b346ee36 | 5 months ago |
|
03dbd91418 | 5 months ago |
|
613e6cae6e | 5 months ago |
|
0b68e1bbcf | 5 months ago |
|
8888262e45 | 5 months ago |
|
0bc530c98e | 5 months ago |
|
2e128056dc | 5 months ago |
|
1a711e1b91 | 5 months ago |
|
abe86002ee | 5 months ago |
|
76dfcb00e4 | 5 months ago |
|
a32bb66e47 | 5 months ago |
|
ff41a1193d | 5 months ago |
|
5ceed08ae0 | 5 months ago |
|
83ba0bdc51 | 5 months ago |
|
c4fb19d01f | 5 months ago |
|
2b1223a080 | 5 months ago |
|
036a144e09 | 5 months ago |
|
97002040a5 | 5 months ago |
|
d32a3e23f0 | 5 months ago |
|
b58de27675 | 5 months ago |
|
7c9f8020d5 | 5 months ago |
|
680b6ea08f | 5 months ago |
|
a97991aa83 | 5 months ago |
|
49ad2d9967 | 5 months ago |
|
a49966f4c9 | 5 months ago |
|
3576cc30c8 | 5 months ago |
|
4172b69816 | 5 months ago |
|
79501df5a2 | 5 months ago |
|
3f474f77d4 | 5 months ago |
|
b6ec4422b4 | 5 months ago |
|
af31a167c6 | 5 months ago |
|
01ef526d08 | 5 months ago |
|
cc5fd0a6a5 | 5 months ago |
|
b3dd169190 | 5 months ago |
|
3eb0ff43c0 | 5 months ago |
|
c41a99ad75 | 5 months ago |
|
5ba0ab3e44 | 5 months ago |
|
73ec6c89d0 | 5 months ago |
|
ca01c74333 | 5 months ago |
|
74ddad69dc | 5 months ago |
|
83f6be1f58 | 5 months ago |
|
2208b03744 | 5 months ago |
|
1fe61bee7b | 5 months ago |
|
e6dd211637 | 5 months ago |
|
227e932624 | 5 months ago |
|
5ca744567c | 5 months ago |
|
da1e64aa53 | 5 months ago |
|
8e0e35532c | 5 months ago |
@ -0,0 +1,607 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/acme/db/nosql"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/authority/provisioner/wire"
|
||||
nosqlDB "github.com/smallstep/nosql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "test.ca.smallstep.com"
|
||||
linkerPrefix = "acme"
|
||||
)
|
||||
|
||||
func newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {
|
||||
p := newProvWithOptions(options)
|
||||
a, ok := p.(*provisioner.ACME)
|
||||
if !ok {
|
||||
t.Fatal("not a valid ACME provisioner")
|
||||
}
|
||||
a.Challenges = []provisioner.ACMEChallenge{
|
||||
provisioner.WIREOIDC_01,
|
||||
provisioner.WIREDPOP_01,
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// TODO(hs): replace with test CA server + acmez based test client for
|
||||
// more realistic integration test?
|
||||
func TestWireIntegration(t *testing.T) {
|
||||
accessTokenSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
accessTokenSignerPEMBlock, err := pemutil.Serialize(accessTokenSignerJWK.Public().Key)
|
||||
require.NoError(t, err)
|
||||
accessTokenSignerPEMBytes := pem.EncodeToMemory(accessTokenSignerPEMBlock)
|
||||
|
||||
accessTokenSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(accessTokenSignerJWK.Algorithm),
|
||||
Key: accessTokenSignerJWK,
|
||||
}, new(jose.SignerOptions))
|
||||
require.NoError(t, err)
|
||||
|
||||
oidcTokenSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
oidcTokenSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(oidcTokenSignerJWK.Algorithm),
|
||||
Key: oidcTokenSignerJWK,
|
||||
}, new(jose.SignerOptions))
|
||||
require.NoError(t, err)
|
||||
|
||||
prov := newWireProvisionerWithOptions(t, &provisioner.Options{
|
||||
X509: &provisioner.X509Options{
|
||||
Template: `{
|
||||
"subject": {
|
||||
"organization": "WireTest",
|
||||
"commonName": {{ toJson .Oidc.name }}
|
||||
},
|
||||
"uris": [{{ toJson .Oidc.preferred_username }}, {{ toJson .Dpop.sub }}],
|
||||
"keyUsage": ["digitalSignature"],
|
||||
"extKeyUsage": ["clientAuth"]
|
||||
}`,
|
||||
},
|
||||
Wire: &wire.Options{
|
||||
OIDC: &wire.OIDCOptions{
|
||||
Provider: &wire.Provider{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
AuthURL: "",
|
||||
TokenURL: "",
|
||||
JWKSURL: "",
|
||||
UserInfoURL: "",
|
||||
Algorithms: []string{"ES256"},
|
||||
},
|
||||
Config: &wire.Config{
|
||||
ClientID: "integration test",
|
||||
SignatureAlgorithms: []string{"ES256"},
|
||||
SkipClientIDCheck: true,
|
||||
SkipExpiryCheck: true,
|
||||
SkipIssuerCheck: true,
|
||||
InsecureSkipSignatureCheck: true, // NOTE: this skips actual token verification
|
||||
Now: time.Now,
|
||||
},
|
||||
TransformTemplate: "",
|
||||
},
|
||||
DPOP: &wire.DPOPOptions{
|
||||
SigningKey: accessTokenSignerPEMBytes,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// mock provisioner and linker
|
||||
ctx := context.Background()
|
||||
ctx = acme.NewProvisionerContext(ctx, prov)
|
||||
ctx = acme.NewLinkerContext(ctx, acme.NewLinker(baseURL, linkerPrefix))
|
||||
|
||||
// create temporary BoltDB file
|
||||
file, err := os.CreateTemp(os.TempDir(), "integration-db-")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("database file name:", file.Name())
|
||||
dbFn := file.Name()
|
||||
err = file.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// open BoltDB
|
||||
rawDB, err := nosqlDB.New(nosqlDB.BBoltDriver, dbFn)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create tables
|
||||
db, err := nosql.New(rawDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
// make DB available to handlers
|
||||
ctx = acme.NewDatabaseContext(ctx, db)
|
||||
|
||||
// simulate signed payloads by making the signing key available in ctx
|
||||
jwk, err := jose.GenerateJWK("OKP", "", "EdDSA", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
ed25519PrivKey, ok := jwk.Key.(ed25519.PrivateKey)
|
||||
require.True(t, ok)
|
||||
|
||||
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
||||
Key: jwk,
|
||||
}, new(jose.SignerOptions))
|
||||
require.NoError(t, err)
|
||||
|
||||
ed25519PubKey, ok := ed25519PrivKey.Public().(ed25519.PublicKey)
|
||||
require.True(t, ok)
|
||||
|
||||
jwk.Key = ed25519PubKey
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
|
||||
// get directory
|
||||
dir := func(ctx context.Context) (dir Directory) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo/bar", http.NoBody)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
GetDirectory(w, req)
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("directory:", dir)
|
||||
|
||||
// get nonce
|
||||
nonce := func(ctx context.Context) (nonce string) {
|
||||
req := httptest.NewRequest(http.MethodGet, dir.NewNonce, http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
addNonce(GetNonce)(w, req)
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||
|
||||
nonce = res.Header["Replay-Nonce"][0]
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("nonce:", nonce)
|
||||
|
||||
// create new account
|
||||
acc := func(ctx context.Context) (acc *acme.Account) {
|
||||
// create payload
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
rawNar, err := json.Marshal(nar)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create account
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: rawNar})
|
||||
req := httptest.NewRequest(http.MethodGet, dir.NewAccount, http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
NewAccount(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusCreated, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
locationParts := strings.Split(res.Header["Location"][0], "/")
|
||||
acc, err = db.GetAccount(ctx, locationParts[len(locationParts)-1])
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
t.Log("account ID:", acc.ID)
|
||||
|
||||
// new order
|
||||
order := func(ctx context.Context) (order *acme.Order) {
|
||||
mockMustAuthority(t, &mockCA{})
|
||||
nor := &NewOrderRequest{
|
||||
Identifiers: []acme.Identifier{
|
||||
{
|
||||
Type: "wireapp-user",
|
||||
Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`,
|
||||
},
|
||||
{
|
||||
Type: "wireapp-device",
|
||||
Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(nor)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
req := httptest.NewRequest("POST", "https://random.local/", http.NoBody)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
NewOrder(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusCreated, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &order)
|
||||
require.NoError(t, err)
|
||||
|
||||
order, err = db.GetOrder(ctx, order.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("authzs IDs:", order.AuthorizationIDs)
|
||||
|
||||
// get authorization
|
||||
getAuthz := func(ctx context.Context, authzID string) (az *acme.Authorization) {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("authzID", authzID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetAuthorization(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &az)
|
||||
require.NoError(t, err)
|
||||
|
||||
az, err = db.GetAuthorization(ctx, authzID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
var azs []*acme.Authorization
|
||||
for _, azID := range order.AuthorizationIDs {
|
||||
az := getAuthz(ctx, azID)
|
||||
azs = append(azs, az)
|
||||
for _, challenge := range az.Challenges {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("chID", challenge.ID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
var payload []byte
|
||||
switch challenge.Type {
|
||||
case acme.WIREDPOP01:
|
||||
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"`
|
||||
}{
|
||||
Claims: jose.Claims{
|
||||
Subject: "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com",
|
||||
},
|
||||
Challenge: "token",
|
||||
Handle: "wireapp://%40alice.smith.qa@example.com",
|
||||
Nonce: "nonce",
|
||||
HTU: "http://issuer.example.com",
|
||||
})
|
||||
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"`
|
||||
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: []string{"test"},
|
||||
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
||||
},
|
||||
Challenge: "token",
|
||||
Cnf: struct {
|
||||
Kid string `json:"kid,omitempty"`
|
||||
}{
|
||||
Kid: jwk.KeyID,
|
||||
},
|
||||
Proof: proof,
|
||||
ClientID: "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com",
|
||||
APIVersion: 5,
|
||||
Scope: "wire_client_id",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
signed, err := accessTokenSigner.Sign(tokenBytes)
|
||||
require.NoError(t, err)
|
||||
accessToken, err := signed.CompactSerialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
p, err := json.Marshal(struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}{
|
||||
AccessToken: accessToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload = p
|
||||
case acme.WIREOIDC01:
|
||||
keyAuth, err := acme.KeyAuthorization("token", jwk)
|
||||
require.NoError(t, err)
|
||||
tokenBytes, err := json.Marshal(struct {
|
||||
jose.Claims
|
||||
Name string `json:"name,omitempty"`
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
KeyAuth string `json:"keyauth"`
|
||||
}{
|
||||
Claims: jose.Claims{
|
||||
Issuer: "https://issuer.example.com",
|
||||
Audience: []string{"test"},
|
||||
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
||||
},
|
||||
Name: "Alice Smith",
|
||||
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
||||
KeyAuth: keyAuth,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
signed, err := oidcTokenSigner.Sign(tokenBytes)
|
||||
require.NoError(t, err)
|
||||
idToken, err := signed.CompactSerialize()
|
||||
require.NoError(t, err)
|
||||
p, err := json.Marshal(struct {
|
||||
IDToken string `json:"id_token"`
|
||||
}{
|
||||
IDToken: idToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload = p
|
||||
default:
|
||||
require.Fail(t, "unexpected challenge payload type")
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payload})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetChallenge(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close() //nolint:gocritic // close the body
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &challenge)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("challenge:", challenge.ID, challenge.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// get/validate challenge simulation
|
||||
updateAz := func(ctx context.Context, az *acme.Authorization) (updatedAz *acme.Authorization) {
|
||||
now := clock.Now().Format(time.RFC3339)
|
||||
for _, challenge := range az.Challenges {
|
||||
challenge.Status = acme.StatusValid
|
||||
challenge.ValidatedAt = now
|
||||
err := db.UpdateChallenge(ctx, challenge)
|
||||
if err != nil {
|
||||
t.Error("updating challenge", challenge.ID, ":", err)
|
||||
}
|
||||
}
|
||||
|
||||
updatedAz, err = db.GetAuthorization(ctx, az.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
for _, az := range azs {
|
||||
updatedAz := updateAz(ctx, az)
|
||||
for _, challenge := range updatedAz.Challenges {
|
||||
t.Log("updated challenge:", challenge.ID, challenge.Status)
|
||||
switch challenge.Type {
|
||||
case acme.WIREOIDC01:
|
||||
err = db.CreateOidcToken(ctx, order.ID, map[string]any{"name": "Smith, Alice M (QA)", "preferred_username": "wireapp://%40alice.smith.qa@example.com"})
|
||||
require.NoError(t, err)
|
||||
case acme.WIREDPOP01:
|
||||
err = db.CreateDpopToken(ctx, order.ID, map[string]any{"sub": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com"})
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
require.Fail(t, "unexpected challenge type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get order
|
||||
updatedOrder := func(ctx context.Context) (updatedOrder *acme.Order) {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("ordID", order.ID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetOrder(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &updatedOrder)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, acme.StatusReady, updatedOrder.Status)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("updated order status:", updatedOrder.Status)
|
||||
|
||||
// finalize order
|
||||
finalizedOrder := func(ctx context.Context) (finalizedOrder *acme.Order) {
|
||||
ca, err := minica.New(minica.WithName("WireTestCA"))
|
||||
require.NoError(t, err)
|
||||
mockMustAuthority(t, &mockCASigner{
|
||||
signer: func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
var (
|
||||
certOptions []x509util.Option
|
||||
)
|
||||
for _, op := range extraOpts {
|
||||
if k, ok := op.(provisioner.CertificateOptions); ok {
|
||||
certOptions = append(certOptions, k.Options(signOpts)...)
|
||||
}
|
||||
}
|
||||
|
||||
x509utilTemplate, err := x509util.NewCertificate(csr, certOptions...)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := x509utilTemplate.GetCertificate()
|
||||
require.NotNil(t, template)
|
||||
|
||||
cert, err := ca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
|
||||
u1, err := url.Parse("wireapp://%40alice.smith.qa@example.com")
|
||||
require.NoError(t, err)
|
||||
u2, err := url.Parse("wireapp://lJGYPz0ZRq2kvc_XpdaDlA%21ed416ce8ecdd9fad@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*url.URL{u1, u2}, cert.URIs)
|
||||
assert.Equal(t, "Smith, Alice M (QA)", cert.Subject.CommonName)
|
||||
|
||||
return []*x509.Certificate{cert, ca.Intermediate}, nil
|
||||
},
|
||||
})
|
||||
|
||||
qUserID, err := url.Parse("wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
qUserName, err := url.Parse("wireapp://%40alice.smith.qa@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
csrTemplate := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"example.com"},
|
||||
ExtraNames: []pkix.AttributeTypeAndValue{
|
||||
{
|
||||
Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},
|
||||
Value: "Smith, Alice M (QA)",
|
||||
},
|
||||
},
|
||||
},
|
||||
URIs: []*url.URL{
|
||||
qUserName,
|
||||
qUserID,
|
||||
},
|
||||
SignatureAlgorithm: x509.PureEd25519,
|
||||
}
|
||||
|
||||
csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv)
|
||||
require.NoError(t, err)
|
||||
|
||||
fr := FinalizeRequest{CSR: base64.RawURLEncoding.EncodeToString(csr)}
|
||||
frRaw, err := json.Marshal(fr)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: frRaw})
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("ordID", order.ID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
FinalizeOrder(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &finalizedOrder)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, acme.StatusValid, finalizedOrder.Status)
|
||||
|
||||
finalizedOrder, err = db.GetOrder(ctx, order.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("finalized order status:", finalizedOrder.Status)
|
||||
}
|
||||
|
||||
type mockCASigner struct {
|
||||
signer func(*x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
}
|
||||
|
||||
func (m *mockCASigner) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
if m.signer == nil {
|
||||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
return m.signer(cr, opts, signOpts...)
|
||||
}
|
||||
|
||||
func (m *mockCASigner) AreSANsAllowed(ctx context.Context, sans []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCASigner) IsRevoked(sn string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *mockCASigner) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCASigner) LoadProvisionerByName(string) (provisioner.Interface, error) {
|
||||
return nil, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,121 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type dbDpopToken struct {
|
||||
ID string `json:"id"`
|
||||
Content []byte `json:"content"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// getDBDpopToken retrieves and unmarshals an DPoP type from the database.
|
||||
func (db *DB) getDBDpopToken(_ context.Context, orderID string) (*dbDpopToken, error) {
|
||||
b, err := db.db.Get(wireDpopTokenTable, []byte(orderID))
|
||||
if err != nil {
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "dpop token %q not found", orderID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed loading dpop token %q: %w", orderID, err)
|
||||
}
|
||||
|
||||
d := new(dbDpopToken)
|
||||
if err := json.Unmarshal(b, d); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling dpop token %q into dbDpopToken: %w", orderID, err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// GetDpopToken retrieves an DPoP from the database.
|
||||
func (db *DB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) {
|
||||
dbDpop, err := db.getDBDpopToken(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dpop := make(map[string]any)
|
||||
err = json.Unmarshal(dbDpop.Content, &dpop)
|
||||
|
||||
return dpop, err
|
||||
}
|
||||
|
||||
// CreateDpopToken creates DPoP resources and saves them to the DB.
|
||||
func (db *DB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error {
|
||||
content, err := json.Marshal(dpop)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling dpop token: %w", err)
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
dbDpop := &dbDpopToken{
|
||||
ID: orderID,
|
||||
Content: content,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := db.save(ctx, orderID, dbDpop, nil, "dpop", wireDpopTokenTable); err != nil {
|
||||
return fmt.Errorf("failed saving dpop token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbOidcToken struct {
|
||||
ID string `json:"id"`
|
||||
Content []byte `json:"content"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// getDBOidcToken retrieves and unmarshals an OIDC id token type from the database.
|
||||
func (db *DB) getDBOidcToken(_ context.Context, orderID string) (*dbOidcToken, error) {
|
||||
b, err := db.db.Get(wireOidcTokenTable, []byte(orderID))
|
||||
if err != nil {
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "oidc token %q not found", orderID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed loading oidc token %q: %w", orderID, err)
|
||||
}
|
||||
|
||||
o := new(dbOidcToken)
|
||||
if err := json.Unmarshal(b, o); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling oidc token %q into dbOidcToken: %w", orderID, err)
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// GetOidcToken retrieves an oidc token from the database.
|
||||
func (db *DB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) {
|
||||
dbOidc, err := db.getDBOidcToken(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idToken := make(map[string]any)
|
||||
err = json.Unmarshal(dbOidc.Content, &idToken)
|
||||
|
||||
return idToken, err
|
||||
}
|
||||
|
||||
// CreateOidcToken creates oidc token resources and saves them to the DB.
|
||||
func (db *DB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error {
|
||||
content, err := json.Marshal(idToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling oidc token: %w", err)
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
dbOidc := &dbOidcToken{
|
||||
ID: orderID,
|
||||
Content: content,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := db.save(ctx, orderID, dbOidc, nil, "oidc", wireOidcTokenTable); err != nil {
|
||||
return fmt.Errorf("failed saving oidc token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,394 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
certificatesdb "github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDB_GetDpopToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
expected map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/acme-not-found": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: &acme.Error{
|
||||
Type: "urn:ietf:params:acme:error:malformed",
|
||||
Status: 400,
|
||||
Detail: "The request message was malformed",
|
||||
Err: errors.New(`dpop token "orderID" not found`),
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbDpopToken{
|
||||
ID: "orderID",
|
||||
Content: []byte("{}"),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireDpopTokenTable, []byte("orderID"), b[1:]) // start at index 1; corrupt JSON data
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed unmarshaling dpop token "orderID" into dbDpopToken: invalid character ':' after top-level value`),
|
||||
}
|
||||
},
|
||||
"fail/db.Get": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equal(t, wireDpopTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed loading dpop token "orderID": fail`),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbDpopToken{
|
||||
ID: "orderID",
|
||||
Content: []byte(`{"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com"}`),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireDpopTokenTable, []byte("orderID"), b)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expected: map[string]any{
|
||||
"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := tc.db.GetDpopToken(context.Background(), tc.orderID)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
ae := &acme.Error{}
|
||||
if errors.As(err, &ae) {
|
||||
ee := &acme.Error{}
|
||||
require.True(t, errors.As(tc.expectedErr, &ee))
|
||||
assert.Equal(t, ee.Detail, ae.Detail)
|
||||
assert.Equal(t, ee.Type, ae.Type)
|
||||
assert.Equal(t, ee.Status, ae.Status)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateDpopToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
dpop map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Save": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
assert.Equal(t, wireDpopTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, false, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
dpop: map[string]any{
|
||||
"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
||||
},
|
||||
expectedErr: errors.New("failed saving dpop token: error saving acme dpop: fail"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
dpop: map[string]any{
|
||||
"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/nil": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
dpop: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := tc.db.CreateDpopToken(context.Background(), tc.orderID, tc.dpop)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
dpop, err := tc.db.getDBDpopToken(context.Background(), tc.orderID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.orderID, dpop.ID)
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(dpop.Content, &m)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.dpop, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetOidcToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
expected map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/acme-not-found": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: &acme.Error{
|
||||
Type: "urn:ietf:params:acme:error:malformed",
|
||||
Status: 400,
|
||||
Detail: "The request message was malformed",
|
||||
Err: errors.New(`oidc token "orderID" not found`),
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbOidcToken{
|
||||
ID: "orderID",
|
||||
Content: []byte("{}"),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireOidcTokenTable, []byte("orderID"), b[1:]) // start at index 1; corrupt JSON data
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed unmarshaling oidc token "orderID" into dbOidcToken: invalid character ':' after top-level value`),
|
||||
}
|
||||
},
|
||||
"fail/db.Get": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equal(t, wireOidcTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed loading oidc token "orderID": fail`),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbOidcToken{
|
||||
ID: "orderID",
|
||||
Content: []byte(`{"name": "Alice Smith", "preferred_username": "@alice.smith"}`),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireOidcTokenTable, []byte("orderID"), b)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expected: map[string]any{
|
||||
"name": "Alice Smith",
|
||||
"preferred_username": "@alice.smith",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := tc.db.GetOidcToken(context.Background(), tc.orderID)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
ae := &acme.Error{}
|
||||
if errors.As(err, &ae) {
|
||||
ee := &acme.Error{}
|
||||
require.True(t, errors.As(tc.expectedErr, &ee))
|
||||
assert.Equal(t, ee.Detail, ae.Detail)
|
||||
assert.Equal(t, ee.Type, ae.Type)
|
||||
assert.Equal(t, ee.Status, ae.Status)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateOidcToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
oidc map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Save": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
assert.Equal(t, wireOidcTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, false, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
oidc: map[string]any{
|
||||
"name": "Alice Smith",
|
||||
"preferred_username": "@alice.smith",
|
||||
},
|
||||
expectedErr: errors.New("failed saving oidc token: error saving acme oidc: fail"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
oidc: map[string]any{
|
||||
"name": "Alice Smith",
|
||||
"preferred_username": "@alice.smith",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/nil": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
oidc: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := tc.db.CreateOidcToken(context.Background(), tc.orderID, tc.oidc)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
oidc, err := tc.db.getDBOidcToken(context.Background(), tc.orderID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.orderID, oidc.ID)
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(oidc.Content, &m)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.oidc, m)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsErrNotFound(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"true ErrNotFound", args{ErrNotFound}, true},
|
||||
{"true sql.ErrNoRows", args{sql.ErrNoRows}, true},
|
||||
{"true wrapped ErrNotFound", args{fmt.Errorf("something failed: %w", ErrNotFound)}, true},
|
||||
{"true wrapped sql.ErrNoRows", args{fmt.Errorf("something failed: %w", sql.ErrNoRows)}, true},
|
||||
{"false other", args{errors.New("not found")}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsErrNotFound(tt.args.err); got != tt.want {
|
||||
t.Errorf("IsErrNotFound() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.step.sm/crypto/kms/uri"
|
||||
)
|
||||
|
||||
type UserID struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
type DeviceID struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
ClientID string `json:"client-id,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
func ParseUserID(data []byte) (id UserID, err error) {
|
||||
err = json.Unmarshal(data, &id)
|
||||
return
|
||||
}
|
||||
|
||||
func ParseDeviceID(data []byte) (id DeviceID, err error) {
|
||||
err = json.Unmarshal(data, &id)
|
||||
return
|
||||
}
|
||||
|
||||
type ClientID struct {
|
||||
Scheme string
|
||||
Username string
|
||||
DeviceID string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// ParseClientID parses a Wire clientID. The ClientID format is as follows:
|
||||
//
|
||||
// "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
||||
//
|
||||
// where '!' is used as a separator between the user id & device id.
|
||||
func ParseClientID(clientID string) (ClientID, error) {
|
||||
clientIDURI, err := uri.Parse(clientID)
|
||||
if err != nil {
|
||||
return ClientID{}, fmt.Errorf("invalid Wire client ID URI %q: %w", clientID, err)
|
||||
}
|
||||
if clientIDURI.Scheme != "wireapp" {
|
||||
return ClientID{}, fmt.Errorf("invalid Wire client ID scheme %q; expected \"wireapp\"", clientIDURI.Scheme)
|
||||
}
|
||||
fullUsername := clientIDURI.User.Username()
|
||||
parts := strings.SplitN(fullUsername, "!", 2)
|
||||
if len(parts) != 2 {
|
||||
return ClientID{}, fmt.Errorf("invalid Wire client ID username %q", fullUsername)
|
||||
}
|
||||
return ClientID{
|
||||
Scheme: clientIDURI.Scheme,
|
||||
Username: parts[0],
|
||||
DeviceID: parts[1],
|
||||
Domain: clientIDURI.Host,
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseUserID(t *testing.T) {
|
||||
ok := `{"name": "Alice Smith", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantWireID UserID
|
||||
expectedErr error
|
||||
}{
|
||||
{name: "ok", data: []byte(ok), wantWireID: UserID{Name: "Alice Smith", Domain: "wire.com", Handle: "wireapp://%40alice_wire@wire.com"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotWireID, err := ParseUserID(tt.data)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantWireID, gotWireID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeviceID(t *testing.T) {
|
||||
ok := `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantWireID DeviceID
|
||||
expectedErr error
|
||||
}{
|
||||
{name: "ok", data: []byte(ok), wantWireID: DeviceID{Name: "device", Domain: "wire.com", ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", Handle: "wireapp://%40alice_wire@wire.com"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotWireID, err := ParseDeviceID(tt.data)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantWireID, gotWireID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClientID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clientID string
|
||||
want ClientID
|
||||
expectedErr error
|
||||
}{
|
||||
{name: "ok", clientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", want: ClientID{Scheme: "wireapp", Username: "CzbfFjDOQrenCbDxVmgnFw", DeviceID: "594930e9d50bb175", Domain: "wire.com"}},
|
||||
{name: "fail/uri", clientID: "bla", expectedErr: errors.New(`invalid Wire client ID URI "bla": error parsing bla: scheme is missing`)},
|
||||
{name: "fail/scheme", clientID: "not-wireapp://bla.com", expectedErr: errors.New(`invalid Wire client ID scheme "not-wireapp"; expected "wireapp"`)},
|
||||
{name: "fail/username", clientID: "wireapp://user@wire.com", expectedErr: errors.New(`invalid Wire client ID username "user"`)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseClientID(tt.clientID)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_CRL(t *testing.T) {
|
||||
data := []byte{1, 2, 3, 4}
|
||||
pemData := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: data,
|
||||
})
|
||||
pemData = bytes.TrimSpace(pemData)
|
||||
emptyPEMData := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: nil,
|
||||
})
|
||||
emptyPEMData = bytes.TrimSpace(emptyPEMData)
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
err error
|
||||
statusCode int
|
||||
crlInfo *authority.CertificateRevocationListInfo
|
||||
expectedBody []byte
|
||||
expectedHeaders http.Header
|
||||
expectedErrorJSON string
|
||||
}{
|
||||
{"ok", "http://example.com/crl", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: data}, data, http.Header{"Content-Type": []string{"application/pkix-crl"}, "Content-Disposition": []string{`attachment; filename="crl.der"`}}, ""},
|
||||
{"ok/pem", "http://example.com/crl?pem=true", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: data}, pemData, http.Header{"Content-Type": []string{"application/x-pem-file"}, "Content-Disposition": []string{`attachment; filename="crl.pem"`}}, ""},
|
||||
{"ok/empty", "http://example.com/crl", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: nil}, nil, http.Header{"Content-Type": []string{"application/pkix-crl"}, "Content-Disposition": []string{`attachment; filename="crl.der"`}}, ""},
|
||||
{"ok/empty-pem", "http://example.com/crl?pem=true", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: nil}, emptyPEMData, http.Header{"Content-Type": []string{"application/x-pem-file"}, "Content-Disposition": []string{`attachment; filename="crl.pem"`}}, ""},
|
||||
{"fail/internal", "http://example.com/crl", errs.Wrap(http.StatusInternalServerError, errors.New("failure"), "authority.GetCertificateRevocationList"), http.StatusInternalServerError, nil, nil, http.Header{}, `{"status":500,"message":"The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info."}`},
|
||||
{"fail/nil", "http://example.com/crl", nil, http.StatusNotFound, nil, nil, http.Header{}, `{"status":404,"message":"no CRL available"}`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{ret1: tt.crlInfo, err: tt.err})
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
req := httptest.NewRequest("GET", tt.url, http.NoBody)
|
||||
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
|
||||
w := httptest.NewRecorder()
|
||||
CRL(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tt.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.statusCode >= 300 {
|
||||
assert.JSONEq(t, tt.expectedErrorJSON, string(bytes.TrimSpace(body)))
|
||||
return
|
||||
}
|
||||
|
||||
// check expected header values
|
||||
for _, h := range []string{"content-type", "content-disposition"} {
|
||||
v := tt.expectedHeaders.Get(h)
|
||||
require.NotEmpty(t, v)
|
||||
|
||||
actual := res.Header.Get(h)
|
||||
assert.Equal(t, v, actual)
|
||||
}
|
||||
|
||||
// check expires header value
|
||||
assert.NotEmpty(t, res.Header.Get("expires"))
|
||||
t1, err := time.Parse(time.RFC1123, res.Header.Get("expires"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.False(t, t1.IsZero())
|
||||
}
|
||||
|
||||
// check body contents
|
||||
assert.Equal(t, tt.expectedBody, bytes.TrimSpace(body))
|
||||
})
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package authority
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"io"
|
||||
|
||||
"go.step.sm/crypto/kms"
|
||||
kmsapi "go.step.sm/crypto/kms/apiv1"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
// Meter wraps the set of defined callbacks for metrics gatherers.
|
||||
type Meter interface {
|
||||
// X509Signed is called whenever an X509 certificate is signed.
|
||||
X509Signed(provisioner.Interface, error)
|
||||
|
||||
// X509Renewed is called whenever an X509 certificate is renewed.
|
||||
X509Renewed(provisioner.Interface, error)
|
||||
|
||||
// X509Rekeyed is called whenever an X509 certificate is rekeyed.
|
||||
X509Rekeyed(provisioner.Interface, error)
|
||||
|
||||
// X509WebhookAuthorized is called whenever an X509 authoring webhook is called.
|
||||
X509WebhookAuthorized(provisioner.Interface, error)
|
||||
|
||||
// X509WebhookEnriched is called whenever an X509 enriching webhook is called.
|
||||
X509WebhookEnriched(provisioner.Interface, error)
|
||||
|
||||
// SSHSigned is called whenever an SSH certificate is signed.
|
||||
SSHSigned(provisioner.Interface, error)
|
||||
|
||||
// SSHRenewed is called whenever an SSH certificate is renewed.
|
||||
SSHRenewed(provisioner.Interface, error)
|
||||
|
||||
// SSHRekeyed is called whenever an SSH certificate is rekeyed.
|
||||
SSHRekeyed(provisioner.Interface, error)
|
||||
|
||||
// SSHWebhookAuthorized is called whenever an SSH authoring webhook is called.
|
||||
SSHWebhookAuthorized(provisioner.Interface, error)
|
||||
|
||||
// SSHWebhookEnriched is called whenever an SSH enriching webhook is called.
|
||||
SSHWebhookEnriched(provisioner.Interface, error)
|
||||
|
||||
// KMSSigned is called per KMS signer signature.
|
||||
KMSSigned(error)
|
||||
}
|
||||
|
||||
// noopMeter implements a noop [Meter].
|
||||
type noopMeter struct{}
|
||||
|
||||
func (noopMeter) SSHRekeyed(provisioner.Interface, error) {}
|
||||
func (noopMeter) SSHRenewed(provisioner.Interface, error) {}
|
||||
func (noopMeter) SSHSigned(provisioner.Interface, error) {}
|
||||
func (noopMeter) SSHWebhookAuthorized(provisioner.Interface, error) {}
|
||||
func (noopMeter) SSHWebhookEnriched(provisioner.Interface, error) {}
|
||||
func (noopMeter) X509Rekeyed(provisioner.Interface, error) {}
|
||||
func (noopMeter) X509Renewed(provisioner.Interface, error) {}
|
||||
func (noopMeter) X509Signed(provisioner.Interface, error) {}
|
||||
func (noopMeter) X509WebhookAuthorized(provisioner.Interface, error) {}
|
||||
func (noopMeter) X509WebhookEnriched(provisioner.Interface, error) {}
|
||||
func (noopMeter) KMSSigned(error) {}
|
||||
|
||||
type instrumentedKeyManager struct {
|
||||
kms.KeyManager
|
||||
meter Meter
|
||||
}
|
||||
|
||||
type instrumentedKeyAndDecrypterManager struct {
|
||||
kms.KeyManager
|
||||
decrypter kmsapi.Decrypter
|
||||
meter Meter
|
||||
}
|
||||
|
||||
func newInstrumentedKeyManager(k kms.KeyManager, m Meter) kms.KeyManager {
|
||||
decrypter, isDecrypter := k.(kmsapi.Decrypter)
|
||||
switch {
|
||||
case isDecrypter:
|
||||
return &instrumentedKeyAndDecrypterManager{&instrumentedKeyManager{k, m}, decrypter, m}
|
||||
default:
|
||||
return &instrumentedKeyManager{k, m}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *instrumentedKeyManager) CreateSigner(req *kmsapi.CreateSignerRequest) (s crypto.Signer, err error) {
|
||||
if s, err = i.KeyManager.CreateSigner(req); err == nil {
|
||||
s = &instrumentedKMSSigner{s, i.meter}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *instrumentedKeyAndDecrypterManager) CreateDecrypter(req *kmsapi.CreateDecrypterRequest) (s crypto.Decrypter, err error) {
|
||||
return i.decrypter.CreateDecrypter(req)
|
||||
}
|
||||
|
||||
type instrumentedKMSSigner struct {
|
||||
crypto.Signer
|
||||
meter Meter
|
||||
}
|
||||
|
||||
func (i *instrumentedKMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
|
||||
signature, err = i.Signer.Sign(rand, digest, opts)
|
||||
i.meter.KMSSigned(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var _ kms.KeyManager = (*instrumentedKeyManager)(nil)
|
||||
var _ kms.KeyManager = (*instrumentedKeyAndDecrypterManager)(nil)
|
||||
var _ kmsapi.Decrypter = (*instrumentedKeyAndDecrypterManager)(nil)
|
@ -0,0 +1,45 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
||||
type DPOPOptions struct {
|
||||
// Public part of the signing key for DPoP access token in PEM format
|
||||
SigningKey []byte `json:"key"`
|
||||
// URI template for the URI the ACME client must call to fetch the DPoP challenge proof (an access token from wire-server)
|
||||
Target string `json:"target"`
|
||||
|
||||
signingKey crypto.PublicKey
|
||||
target *template.Template
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) GetSigningKey() crypto.PublicKey {
|
||||
return o.signingKey
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) EvaluateTarget(deviceID string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {
|
||||
return "", fmt.Errorf("failed executing dpop template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) validateAndInitialize() (err error) {
|
||||
o.signingKey, err = pemutil.Parse(o.SigningKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing key: %w", err)
|
||||
}
|
||||
o.target, err = template.New("DeviceID").Parse(o.Target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing DPoP template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
IssuerURL string `json:"issuer,omitempty"`
|
||||
AuthURL string `json:"authorization_endpoint,omitempty"`
|
||||
TokenURL string `json:"token_endpoint,omitempty"`
|
||||
JWKSURL string `json:"jwks_uri,omitempty"`
|
||||
UserInfoURL string `json:"userinfo_endpoint,omitempty"`
|
||||
Algorithms []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
SignatureAlgorithms []string `json:"signatureAlgorithms,omitempty"`
|
||||
|
||||
// the properties below are only used for testing
|
||||
SkipClientIDCheck bool `json:"-"`
|
||||
SkipExpiryCheck bool `json:"-"`
|
||||
SkipIssuerCheck bool `json:"-"`
|
||||
InsecureSkipSignatureCheck bool `json:"-"`
|
||||
Now func() time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type OIDCOptions struct {
|
||||
Provider *Provider `json:"provider,omitempty"`
|
||||
Config *Config `json:"config,omitempty"`
|
||||
TransformTemplate string `json:"transform,omitempty"`
|
||||
|
||||
oidcProviderConfig *oidc.ProviderConfig
|
||||
target *template.Template
|
||||
transform *template.Template
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetProvider(ctx context.Context) *oidc.Provider {
|
||||
if o == nil || o.Provider == nil || o.oidcProviderConfig == nil {
|
||||
return nil
|
||||
}
|
||||
return o.oidcProviderConfig.NewProvider(ctx)
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetConfig() *oidc.Config {
|
||||
if o == nil || o.Config == nil {
|
||||
return &oidc.Config{}
|
||||
}
|
||||
|
||||
return &oidc.Config{
|
||||
ClientID: o.Config.ClientID,
|
||||
SupportedSigningAlgs: o.Config.SignatureAlgorithms,
|
||||
SkipClientIDCheck: o.Config.SkipClientIDCheck,
|
||||
SkipExpiryCheck: o.Config.SkipExpiryCheck,
|
||||
SkipIssuerCheck: o.Config.SkipIssuerCheck,
|
||||
Now: o.Config.Now,
|
||||
InsecureSkipSignatureCheck: o.Config.InsecureSkipSignatureCheck,
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTemplate = `{"name": "{{ .name }}", "preferred_username": "{{ .preferred_username }}"}`
|
||||
|
||||
func (o *OIDCOptions) validateAndInitialize() (err error) {
|
||||
if o.Provider == nil {
|
||||
return errors.New("provider not set")
|
||||
}
|
||||
if o.Provider.IssuerURL == "" {
|
||||
return errors.New("issuer URL must not be empty")
|
||||
}
|
||||
|
||||
o.oidcProviderConfig, err = toOIDCProviderConfig(o.Provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creationg OIDC provider config: %w", err)
|
||||
}
|
||||
|
||||
o.target, err = template.New("DeviceID").Parse(o.Provider.IssuerURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing OIDC template: %w", err)
|
||||
}
|
||||
|
||||
o.transform, err = parseTransform(o.TransformTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing OIDC transformation template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTransform(transformTemplate string) (*template.Template, error) {
|
||||
if transformTemplate == "" {
|
||||
transformTemplate = defaultTemplate
|
||||
}
|
||||
|
||||
return template.New("transform").Funcs(x509util.GetFuncMap()).Parse(transformTemplate)
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) EvaluateTarget(deviceID string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {
|
||||
return "", fmt.Errorf("failed executing OIDC template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) Transform(v map[string]any) (map[string]any, error) {
|
||||
if o.transform == nil || v == nil {
|
||||
return v, nil
|
||||
}
|
||||
// TODO(hs): add support for extracting error message from template "fail" function?
|
||||
buf := new(bytes.Buffer)
|
||||
if err := o.transform.Execute(buf, v); err != nil {
|
||||
return nil, fmt.Errorf("failed executing OIDC transformation: %w", err)
|
||||
}
|
||||
var r map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &r); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling transformed OIDC token: %w", err)
|
||||
}
|
||||
// add original claims if not yet in the transformed result
|
||||
for key, value := range v {
|
||||
if _, ok := r[key]; !ok {
|
||||
r[key] = value
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func toOIDCProviderConfig(in *Provider) (*oidc.ProviderConfig, error) {
|
||||
issuerURL, err := url.Parse(in.IssuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed parsing issuer URL: %w", err)
|
||||
}
|
||||
// Removes query params from the URL because we use it as a way to notify client about the actual OAuth ClientId
|
||||
// for this provisioner.
|
||||
// This URL is going to look like: "https://idp:5556/dex?clientid=foo"
|
||||
// If we don't trim the query params here i.e. 'clientid' then the idToken verification is going to fail because
|
||||
// the 'iss' claim of the idToken will be "https://idp:5556/dex"
|
||||
issuerURL.RawQuery = ""
|
||||
issuerURL.Fragment = ""
|
||||
return &oidc.ProviderConfig{
|
||||
IssuerURL: issuerURL.String(),
|
||||
AuthURL: in.AuthURL,
|
||||
TokenURL: in.TokenURL,
|
||||
UserInfoURL: in.UserInfoURL,
|
||||
JWKSURL: in.JWKSURL,
|
||||
Algorithms: in.Algorithms,
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOIDCOptions_Transform(t *testing.T) {
|
||||
defaultTransform, err := parseTransform(``)
|
||||
require.NoError(t, err)
|
||||
swapTransform, err := parseTransform(`{"name": "{{ .preferred_username }}", "preferred_username": "{{ .name }}"}`)
|
||||
require.NoError(t, err)
|
||||
funcTransform, err := parseTransform(`{"name": "{{ .name }}", "preferred_username": "{{ first .usernames }}"}`)
|
||||
require.NoError(t, err)
|
||||
type fields struct {
|
||||
transform *template.Template
|
||||
}
|
||||
type args struct {
|
||||
v map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want map[string]any
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "ok/no-transform",
|
||||
fields: fields{
|
||||
transform: nil,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok/empty-data",
|
||||
fields: fields{
|
||||
transform: nil,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{},
|
||||
},
|
||||
want: map[string]any{},
|
||||
},
|
||||
{
|
||||
name: "ok/default-transform",
|
||||
fields: fields{
|
||||
transform: defaultTransform,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok/swap-transform",
|
||||
fields: fields{
|
||||
transform: swapTransform,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Preferred",
|
||||
"preferred_username": "Example",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok/transform-with-functions",
|
||||
fields: fields{
|
||||
transform: funcTransform,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"usernames": []string{"name-1", "name-2", "name-3"},
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "name-1",
|
||||
"usernames": []string{"name-1", "name-2", "name-3"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &OIDCOptions{
|
||||
transform: tt.fields.transform,
|
||||
}
|
||||
got, err := o.Transform(tt.args.v)
|
||||
if tt.expectedErr != nil {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Options holds the Wire ACME extension options
|
||||
type Options struct {
|
||||
OIDC *OIDCOptions `json:"oidc,omitempty"`
|
||||
DPOP *DPOPOptions `json:"dpop,omitempty"`
|
||||
}
|
||||
|
||||
// GetOIDCOptions returns the OIDC options.
|
||||
func (o *Options) GetOIDCOptions() *OIDCOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.OIDC
|
||||
}
|
||||
|
||||
// GetDPOPOptions returns the DPoP options.
|
||||
func (o *Options) GetDPOPOptions() *DPOPOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.DPOP
|
||||
}
|
||||
|
||||
// Validate validates and initializes the Wire OIDC and DPoP options.
|
||||
//
|
||||
// TODO(hs): find a good way to perform this only once.
|
||||
func (o *Options) Validate() error {
|
||||
if oidc := o.GetOIDCOptions(); oidc != nil {
|
||||
if err := oidc.validateAndInitialize(); err != nil {
|
||||
return fmt.Errorf("failed initializing OIDC options: %w", err)
|
||||
}
|
||||
} else {
|
||||
return errors.New("no OIDC options available")
|
||||
}
|
||||
|
||||
if dpop := o.GetDPOPOptions(); dpop != nil {
|
||||
if err := dpop.validateAndInitialize(); err != nil {
|
||||
return fmt.Errorf("failed initializing DPoP options: %w", err)
|
||||
}
|
||||
} else {
|
||||
return errors.New("no DPoP options available")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptions_Validate(t *testing.T) {
|
||||
key := []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
|
||||
-----END PUBLIC KEY-----`)
|
||||
|
||||
type fields struct {
|
||||
OIDC *OIDCOptions
|
||||
DPOP *DPOPOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: key,
|
||||
},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "fail/no-oidc-options",
|
||||
fields: fields{
|
||||
OIDC: nil,
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New("no OIDC options available"),
|
||||
},
|
||||
{
|
||||
name: "fail/empty-issuer-url",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New("failed initializing OIDC options: issuer URL must not be empty"),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-issuer-url",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "\x00",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing OIDC options: failed creationg OIDC provider config: failed parsing issuer URL: parse "\x00": net/url: invalid control character in URL`),
|
||||
},
|
||||
{
|
||||
name: "fail/issuer-url-template",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://issuer.example.com/{{}",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC template: template: DeviceID:1: unexpected "}" in command`),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-transform-template",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
TransformTemplate: "{{}",
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: key,
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC transformation template: template: transform:1: unexpected "}" in command`),
|
||||
},
|
||||
{
|
||||
name: "fail/no-dpop-options",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: nil,
|
||||
},
|
||||
expectedErr: errors.New("no DPoP options available"),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-key",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: []byte{0x00},
|
||||
Target: "",
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing DPoP options: failed parsing key: error decoding PEM: not a valid PEM encoded block`),
|
||||
},
|
||||
{
|
||||
name: "fail/target-template",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: key,
|
||||
Target: "{{}",
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing DPoP options: failed parsing DPoP template: template: DeviceID:1: unexpected "}" in command`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &Options{
|
||||
OIDC: tt.fields.OIDC,
|
||||
DPOP: tt.fields.DPOP,
|
||||
}
|
||||
err := o.Validate()
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue