90 lines
3.1 KiB
Go
90 lines
3.1 KiB
Go
package deviceauth
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"errors"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestDatabaseSecretResolverImportsEncryptedCredential(t *testing.T) {
|
||
|
|
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
||
|
|
if databaseURL == "" {
|
||
|
|
t.Skip("TEST_DATABASE_URL is not set")
|
||
|
|
}
|
||
|
|
ctx := context.Background()
|
||
|
|
pool, err := pgxpool.New(ctx, databaseURL)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer pool.Close()
|
||
|
|
|
||
|
|
deviceID := uuid.NewString()
|
||
|
|
publicID := uuid.NewString()
|
||
|
|
reference := "database-test://" + publicID
|
||
|
|
serial := sha256.Sum256([]byte(publicID))
|
||
|
|
_, err = pool.Exec(ctx, `
|
||
|
|
INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref)
|
||
|
|
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001'::uuid,$3,'database-credential-test','registered',$4)`,
|
||
|
|
deviceID, publicID, hex.EncodeToString(serial[:]), reference)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer func() {
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM device_credential WHERE device_id=$1::uuid`, deviceID)
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM device WHERE id=$1::uuid`, deviceID)
|
||
|
|
}()
|
||
|
|
|
||
|
|
const masterKey = "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
|
||
|
|
secret := []byte("database-device-secret-at-least-32-bytes")
|
||
|
|
resolver, err := NewDatabaseSecretResolver(pool, masterKey)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := resolver.ImportMissingLegacyCredentials(ctx, map[string][]byte{reference: secret}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
resolved, err := resolver.Resolve(ctx, reference)
|
||
|
|
if err != nil || !bytes.Equal(resolved, secret) {
|
||
|
|
t.Fatalf("resolved credential does not match: len=%d err=%v", len(resolved), err)
|
||
|
|
}
|
||
|
|
var encrypted []byte
|
||
|
|
if err := pool.QueryRow(ctx, `SELECT encrypted_secret FROM device_credential WHERE device_id=$1::uuid`, deviceID).Scan(&encrypted); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if bytes.Contains(encrypted, secret) || bytes.Equal(encrypted, secret) {
|
||
|
|
t.Fatal("database stored plaintext device credential")
|
||
|
|
}
|
||
|
|
wrongResolver, err := NewDatabaseSecretResolver(pool, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := wrongResolver.Resolve(ctx, reference); !errors.Is(err, ErrCredentialInvalid) {
|
||
|
|
t.Fatalf("wrong master key error=%v", err)
|
||
|
|
}
|
||
|
|
if _, err := resolver.Provision(ctx, publicID, []byte("replacement-device-secret-at-least-32-bytes"), false); !errors.Is(err, ErrCredentialAlreadyExists) {
|
||
|
|
t.Fatalf("duplicate provision error=%v", err)
|
||
|
|
}
|
||
|
|
replacement := []byte("replacement-device-secret-at-least-32-bytes")
|
||
|
|
version, err := resolver.Provision(ctx, publicID, replacement, true)
|
||
|
|
if err != nil || version != 2 {
|
||
|
|
t.Fatalf("rotate version=%d err=%v", version, err)
|
||
|
|
}
|
||
|
|
resolved, err = resolver.Resolve(ctx, reference)
|
||
|
|
if err != nil || !bytes.Equal(resolved, replacement) {
|
||
|
|
t.Fatalf("rotated credential does not match: len=%d err=%v", len(resolved), err)
|
||
|
|
}
|
||
|
|
if err := resolver.Revoke(ctx, publicID); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := resolver.Resolve(ctx, reference); !errors.Is(err, ErrCredentialInvalid) {
|
||
|
|
t.Fatalf("revoked credential error=%v", err)
|
||
|
|
}
|
||
|
|
}
|