diff --git a/README.md b/README.md
index b963eb3f52284066eb98b5009ce6250ea86b7897..3efa276282dbe23c0029fc7271c8f3f00217958d 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,9 @@ It is developed using the [Goa v3](https://goa.design/) framework
 and uses the [Go OPA framework](https://github.com/open-policy-agent/opa) 
 as a library.
 
-While the service is up and running, you can see a live Swagger API 
-description at `servicehost:serviceport/swagger-ui`. In the local docker-compose 
-environment, the Swagger URL is available at http://localhost:8081/swagger-ui/ 
+[Swagger OpenAPI documentation](https://gitlab.eclipse.org/eclipse/xfsc/tsa/policy/-/blob/main/gen/http/openapi3.json)
+
+In the local docker-compose environment, the Swagger URL is available at http://localhost:8081/swagger-ui/ 
 
 ### High-level Overview
 
diff --git a/cmd/policy/main.go b/cmd/policy/main.go
index df96345889461bb0379e4c6653cc8cfac345eeaf..b1ac8800ac4af97777f1333cd226a397ae48e31a 100644
--- a/cmd/policy/main.go
+++ b/cmd/policy/main.go
@@ -166,7 +166,7 @@ func main() {
 		healthSvc goahealth.Service
 	)
 	{
-		policySvc = policy.New(storage, regocache, cache, signer, logger)
+		policySvc = policy.New(storage, regocache, cache, signer, cfg.ExternalAddr, httpClient, logger)
 		healthSvc = health.New(Version)
 	}
 
diff --git a/design/design.go b/design/design.go
index 3bbaa145230e0eecae68f2569e63b18cc5e2f818..e205c3467dab5cf6dd67892fd35db579c3794fd2 100644
--- a/design/design.go
+++ b/design/design.go
@@ -83,6 +83,34 @@ var _ = Service("policy", func() {
 		})
 	})
 
+	Method("ImportBundle", func() {
+		Description("Import a signed policy bundle.")
+		Payload(func() {
+			Attribute("length", Int)
+		})
+		Result(Any)
+		HTTP(func() {
+			POST("/policy/import")
+			Header("length:Content-Length")
+
+			SkipRequestBodyEncodeDecode()
+
+			Response(StatusOK)
+			Response(StatusForbidden)
+			Response(StatusInternalServerError)
+		})
+	})
+
+	Method("PolicyPublicKey", func() {
+		Description("PolicyPublicKey returns the public key in JWK format which must be used to verify a signed policy bundle.")
+		Payload(PolicyPublicKeyRequest)
+		Result(Any)
+		HTTP(func() {
+			GET("/policy/{repository}/{group}/{policyName}/{version}/key")
+			Response(StatusOK)
+		})
+	})
+
 	Method("ListPolicies", func() {
 		Description("List policies from storage with optional filters.")
 		Payload(PoliciesRequest)
diff --git a/design/types.go b/design/types.go
index aefd61be4fd9326fb437618cd2e9c9916d6bcad6..c7d384a5fec9304af095cbd19582727edcd0e5ee 100644
--- a/design/types.go
+++ b/design/types.go
@@ -69,6 +69,22 @@ var ExportBundleResult = Type("ExportBundleResult", func() {
 	Required("content-type", "content-length", "content-disposition")
 })
 
+var PolicyPublicKeyRequest = Type("PolicyPublicKeyRequest", func() {
+	Field(1, "repository", String, "Policy repository.", func() {
+		Example("policies")
+	})
+	Field(2, "group", String, "Policy group.", func() {
+		Example("example")
+	})
+	Field(3, "policyName", String, "Policy name.", func() {
+		Example("returnDID")
+	})
+	Field(4, "version", String, "Policy version.", func() {
+		Example("1.0")
+	})
+	Required("repository", "group", "policyName", "version")
+})
+
 var Policy = Type("Policy", func() {
 	Field(1, "repository", String, "Policy repository.")
 	Field(2, "policyName", String, "Policy name.")
diff --git a/internal/clients/signer/client.go b/internal/clients/signer/client.go
index b078b0be004bf73a5c247deaf1781ab2b8bdc607..d9509f4f5e04552afe9a2238438a5395d0efbe17 100644
--- a/internal/clients/signer/client.go
+++ b/internal/clients/signer/client.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 
@@ -78,6 +79,35 @@ func (c *Client) Sign(ctx context.Context, namespace, key string, data []byte) (
 	return base64.StdEncoding.DecodeString(result.Signature)
 }
 
+func (c *Client) Key(ctx context.Context, namespace string, key string) (any, error) {
+	if c.addr == "" {
+		return nil, errors.New(errors.ServiceUnavailable, "signer address is not set")
+	}
+
+	keyPath := fmt.Sprintf("/v1/jwk/%s/%s", namespace, key)
+	req, err := http.NewRequestWithContext(ctx, "GET", c.addr+keyPath, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close() // nolint:errcheck
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, errors.New(errors.GetKind(resp.StatusCode), getErrorBody(resp))
+	}
+
+	var pubkey map[string]interface{}
+	if err := json.NewDecoder(resp.Body).Decode(&pubkey); err != nil {
+		return nil, err
+	}
+
+	return pubkey, nil
+}
+
 func getErrorBody(resp *http.Response) string {
 	body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
 	if err != nil {
diff --git a/internal/config/config.go b/internal/config/config.go
index eaaaadb782e8a4cda17c4056dacbfadc9b6114bc..d3996c12cc2ec9675b5b70711d024945684da77a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -18,6 +18,12 @@ type Config struct {
 	Nats        natsConfig
 	Policy      policyConfig
 
+	// ExternalAddr specifies the external address where
+	// the policy service could be reached, so that
+	// policy bundle verifiers can fetch public keys
+	// for verification.
+	ExternalAddr string `envconfig:"EXTERNAL_HTTP_ADDR" default:"http://localhost:8081"`
+
 	LogLevel string `envconfig:"LOG_LEVEL" default:"INFO"`
 }
 
diff --git a/internal/service/policy/bundle.go b/internal/service/policy/bundle.go
index f7d2458c59cade14ae46f0441718177a63e71fd0..875f4f77701a23cddc4d6f14106e515069aef949 100644
--- a/internal/service/policy/bundle.go
+++ b/internal/service/policy/bundle.go
@@ -4,9 +4,12 @@ import (
 	"archive/zip"
 	"bytes"
 	"encoding/json"
+	"fmt"
+	"io"
 	"strings"
 	"time"
 
+	"gitlab.eclipse.org/eclipse/xfsc/tsa/golib/errors"
 	"gitlab.eclipse.org/eclipse/xfsc/tsa/policy/internal/storage"
 )
 
@@ -24,6 +27,7 @@ type Metadata struct {
 		Locked     bool      `json:"locked"`
 		LastUpdate time.Time `json:"lastUpdate"`
 	} `json:"policy"`
+	PublicKeyURL string `json:"publicKeyURL"`
 }
 
 func (s *Service) createPolicyBundle(policy *storage.Policy) ([]byte, error) {
@@ -62,6 +66,14 @@ func (s *Service) createPolicyBundle(policy *storage.Policy) ([]byte, error) {
 		})
 	}
 
+	// prepare json schema config file
+	if strings.TrimSpace(policy.OutputSchema) != "" {
+		files = append(files, ZipFile{
+			Name:    "output-schema.json",
+			Content: []byte(policy.OutputSchema),
+		})
+	}
+
 	return s.createZipArchive(files)
 }
 
@@ -73,6 +85,7 @@ func (s *Service) createMetadata(policy *storage.Policy) ([]byte, error) {
 	meta.Policy.Repository = policy.Repository
 	meta.Policy.Locked = policy.Locked
 	meta.Policy.LastUpdate = policy.LastUpdate
+	meta.PublicKeyURL = s.policyPublicKeyURL(policy)
 	return json.Marshal(meta)
 }
 
@@ -97,3 +110,72 @@ func (s *Service) createZipArchive(files []ZipFile) ([]byte, error) {
 
 	return buf.Bytes(), nil
 }
+
+func (s *Service) unzip(archive []byte) ([]ZipFile, error) {
+	r, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
+	if err != nil {
+		return nil, err
+	}
+
+	var files []ZipFile
+	for _, file := range r.File {
+		reader, err := file.Open()
+		if err != nil {
+			return nil, err
+		}
+		content, err := io.ReadAll(reader)
+		if err != nil {
+			return nil, err
+		}
+		files = append(files, ZipFile{
+			Name:    file.Name,
+			Content: content,
+		})
+	}
+
+	return files, nil
+}
+
+func (s *Service) policyPublicKeyURL(policy *storage.Policy) string {
+	return fmt.Sprintf("%s/policy/%s/%s/%s/%s/key",
+		s.externalHostname,
+		policy.Repository,
+		policy.Group,
+		policy.Name,
+		policy.Version,
+	)
+}
+
+func (s *Service) policyFromBundle(bundle []byte) (*storage.Policy, error) {
+	bundleFiles, err := s.unzip(bundle)
+	if err != nil {
+		return nil, errors.New("error unzipping bundle archive", err)
+	}
+
+	var policy storage.Policy
+	for _, f := range bundleFiles {
+		switch f.Name {
+		case "metadata.json":
+			var metadata Metadata
+			if err := json.Unmarshal(f.Content, &metadata); err != nil {
+				return nil, err
+			}
+			policy.Repository = metadata.Policy.Repository
+			policy.Group = metadata.Policy.Group
+			policy.Name = metadata.Policy.Name
+			policy.Version = metadata.Policy.Version
+			policy.Locked = metadata.Policy.Locked
+			policy.LastUpdate = metadata.Policy.LastUpdate
+		case "policy.rego":
+			policy.Rego = string(f.Content)
+		case "data.json":
+			policy.Data = string(f.Content)
+		case "data-config.json":
+			policy.DataConfig = string(f.Content)
+		case "output-schema.json":
+			policy.OutputSchema = string(f.Content)
+		}
+	}
+
+	return &policy, nil
+}
diff --git a/internal/service/policy/bundle_signer.go b/internal/service/policy/bundle_signer.go
deleted file mode 100644
index 8e6b9bcb0e3aaad6041874ba4a204f83bc1e8eb8..0000000000000000000000000000000000000000
--- a/internal/service/policy/bundle_signer.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package policy
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/lestrrat-go/jwx/v2/jws"
-)
-
-const JwaVaultSignature jwa.SignatureAlgorithm = "VaultSignature"
-
-// signAdapter implements the jws.Signer interface so that it
-// can be used with the lestrrat-go library. Under the hood it
-// does the signing by calling an external signer service.
-type signAdapter struct {
-	signer Signer
-}
-
-type signAdapterKey struct {
-	Namespace string
-	Key       string
-}
-
-func (a *signAdapter) Sign(data []byte, key interface{}) ([]byte, error) {
-	signKey, ok := key.(*signAdapterKey)
-	if !ok {
-		return nil, fmt.Errorf("unexpected sign adapter key: %T", key)
-	}
-
-	return a.signer.Sign(context.Background(), signKey.Namespace, signKey.Key, data)
-}
-
-func (a *signAdapter) Algorithm() jwa.SignatureAlgorithm {
-	return JwaVaultSignature
-}
-
-func (s *Service) sign(namespace, key string, data []byte) ([]byte, error) {
-	signature, err := jws.Sign(
-		nil,
-		jws.WithKey(JwaVaultSignature, &signAdapterKey{
-			Namespace: namespace,
-			Key:       key,
-		}),
-		jws.WithDetachedPayload(data),
-	)
-
-	return signature, err
-}
diff --git a/internal/service/policy/bundle_test.go b/internal/service/policy/bundle_test.go
index b32e12c2b7bd6c02f2eab81507000c3dcbd99e70..b52ff73a9874e8c9fa24fddca704ff3b2f7f698b 100644
--- a/internal/service/policy/bundle_test.go
+++ b/internal/service/policy/bundle_test.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"io"
+	"net/http"
 	"testing"
 	"time"
 
@@ -44,10 +45,11 @@ var testMetadata = Metadata{
 		Locked:     true,
 		LastUpdate: time.Date(2023, 11, 7, 1, 0, 0, 0, time.UTC),
 	},
+	PublicKeyURL: "https://policyservice.com/policy/myrepo/example/mypolicy/1.0/key",
 }
 
 func TestPolicy_createPolicyBundle(t *testing.T) {
-	svc := New(nil, nil, nil, nil, zap.NewNop())
+	svc := New(nil, nil, nil, nil, "https://policyservice.com", http.DefaultClient, zap.NewNop())
 	bundle, err := svc.createPolicyBundle(testPolicy)
 	assert.NoError(t, err)
 	assert.NotNil(t, bundle)
@@ -94,3 +96,15 @@ func TestPolicy_createPolicyBundle(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, `{"cfg":"static data config"}`, string(dataConfig))
 }
+
+func TestPolicy_policyFromBundle(t *testing.T) {
+	svc := New(nil, nil, nil, nil, "https://policyservice.com", http.DefaultClient, zap.NewNop())
+	bundle, err := svc.createPolicyBundle(testPolicy)
+	require.NoError(t, err)
+	require.NotNil(t, bundle)
+
+	policy, err := svc.policyFromBundle(bundle)
+	require.NoError(t, err)
+	require.NotNil(t, policy)
+	assert.Equal(t, testPolicy, policy)
+}
diff --git a/internal/service/policy/bundle_verify.go b/internal/service/policy/bundle_verify.go
new file mode 100644
index 0000000000000000000000000000000000000000..88876b487b6db1f95e28eddcf46df879bab24514
--- /dev/null
+++ b/internal/service/policy/bundle_verify.go
@@ -0,0 +1,136 @@
+package policy
+
+import (
+	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/rsa"
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+func (s *Service) verifyBundle(ctx context.Context, files []ZipFile) error {
+	policyBundleFile := files[0]
+	signatureFile := files[1]
+
+	if policyBundleFile.Name != BundleFilename {
+		return fmt.Errorf("verify bundle: invalid bundle filename: %q", files[0].Name)
+	}
+
+	if signatureFile.Name != BundleSignatureFilename {
+		return fmt.Errorf("verify bundle: invalid signature filename: %q", files[1].Name)
+	}
+
+	bundleFiles, err := s.unzip(policyBundleFile.Content)
+	if err != nil {
+		return err
+	}
+
+	if len(bundleFiles) == 0 || bundleFiles[0].Name != "metadata.json" {
+		return fmt.Errorf("invalid bundle")
+	}
+
+	var metadata Metadata
+	if err := json.Unmarshal(bundleFiles[0].Content, &metadata); err != nil {
+		return fmt.Errorf("failed to unmarshal metadata: %v", err)
+	}
+
+	// whitelist is insecure to allow fetching keys from arbitrary external locations
+	// TODO: this can be fine-tuned with configuration variable so that organizations
+	// can specify trusted import locations.
+	keyset, err := jwk.Fetch(ctx,
+		metadata.PublicKeyURL,
+		jwk.WithHTTPClient(s.httpClient),
+		jwk.WithFetchWhitelist(jwk.InsecureWhitelist{}),
+	)
+	if err != nil {
+		return fmt.Errorf("verify bundle: %v", err)
+	}
+
+	// we expect to receive a single verification key
+	verKey, ok := keyset.Key(0)
+	if !ok {
+		return fmt.Errorf("cannot get bundle verification key")
+	}
+
+	// the payload that is signed on policy export is the sha256 hash of the
+	// policy bundle zip file itself, so this is the payload that should be verified
+	payload := sha256.Sum256(policyBundleFile.Content)
+
+	switch kt := verKey.KeyType(); kt {
+	case jwa.EC:
+		err = s.verifyECDSA(payload[:], signatureFile.Content, verKey)
+	case jwa.OKP:
+		err = s.verifyED25519(payload[:], signatureFile.Content, verKey)
+	case jwa.RSA:
+		err = s.verifyRSA(payload[:], signatureFile.Content, verKey)
+	default:
+		return fmt.Errorf("unsupported public key type: %v", kt)
+	}
+
+	return err
+}
+
+func (s *Service) verifyECDSA(payload []byte, signature []byte, key jwk.Key) error {
+	// convert key from JWK to ecdsa.PublicKey
+	var ecdsaKey ecdsa.PublicKey
+	if err := key.Raw(&ecdsaKey); err != nil {
+		return err
+	}
+
+	// hash function is always sha-256 by default when Hashicorp Vault signs
+	// data with ECDSA keys and no specific/different hash function is selected
+	// by the client.
+	hash := sha256.Sum256(payload)
+
+	// ECDSA signatures returned by Hashicorp Vault are ASN encoded
+	valid := ecdsa.VerifyASN1(&ecdsaKey, hash[:], signature)
+	if !valid {
+		return fmt.Errorf("invalid signature")
+	}
+
+	return nil
+}
+
+func (s *Service) verifyED25519(payload []byte, signature []byte, key jwk.Key) error {
+	// convert key from JWK to ed25519.PublicKey
+	var ed25519Key ed25519.PublicKey
+	if err := key.Raw(&ed25519Key); err != nil {
+		return err
+	}
+
+	// for ed25519 signatures we must not specifically hash the payload
+	// as this signature algorithm is using its own hash function internally
+	valid := ed25519.Verify(ed25519Key, payload, signature)
+	if !valid {
+		return fmt.Errorf("invalid signature")
+	}
+
+	return nil
+}
+
+func (s *Service) verifyRSA(payload []byte, signature []byte, key jwk.Key) error {
+	// convert key from JWK to rsa.PublicKey
+	var rsaKey rsa.PublicKey
+	if err := key.Raw(&rsaKey); err != nil {
+		return err
+	}
+
+	// hash function is always sha-256 by default when Hashicorp Vault signs
+	// data with RSA keys and no specific/different hash function is selected
+	// by the client.
+	hash := sha256.Sum256(payload)
+
+	err := rsa.VerifyPSS(&rsaKey, crypto.SHA256, hash[:], signature, nil)
+	if err != nil {
+		return fmt.Errorf("invalid signature: %v", err)
+
+	}
+
+	return nil
+}
diff --git a/internal/service/policy/policyfakes/fake_signer.go b/internal/service/policy/policyfakes/fake_signer.go
index 3eac58f165dd71776cea3b11827fbb5743428348..73f80bc19fccb0d79dfb04002df63b0fa12cbcbb 100644
--- a/internal/service/policy/policyfakes/fake_signer.go
+++ b/internal/service/policy/policyfakes/fake_signer.go
@@ -9,6 +9,21 @@ import (
 )
 
 type FakeSigner struct {
+	KeyStub        func(context.Context, string, string) (any, error)
+	keyMutex       sync.RWMutex
+	keyArgsForCall []struct {
+		arg1 context.Context
+		arg2 string
+		arg3 string
+	}
+	keyReturns struct {
+		result1 any
+		result2 error
+	}
+	keyReturnsOnCall map[int]struct {
+		result1 any
+		result2 error
+	}
 	SignStub        func(context.Context, string, string, []byte) ([]byte, error)
 	signMutex       sync.RWMutex
 	signArgsForCall []struct {
@@ -29,6 +44,72 @@ type FakeSigner struct {
 	invocationsMutex sync.RWMutex
 }
 
+func (fake *FakeSigner) Key(arg1 context.Context, arg2 string, arg3 string) (any, error) {
+	fake.keyMutex.Lock()
+	ret, specificReturn := fake.keyReturnsOnCall[len(fake.keyArgsForCall)]
+	fake.keyArgsForCall = append(fake.keyArgsForCall, struct {
+		arg1 context.Context
+		arg2 string
+		arg3 string
+	}{arg1, arg2, arg3})
+	stub := fake.KeyStub
+	fakeReturns := fake.keyReturns
+	fake.recordInvocation("Key", []interface{}{arg1, arg2, arg3})
+	fake.keyMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2, arg3)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeSigner) KeyCallCount() int {
+	fake.keyMutex.RLock()
+	defer fake.keyMutex.RUnlock()
+	return len(fake.keyArgsForCall)
+}
+
+func (fake *FakeSigner) KeyCalls(stub func(context.Context, string, string) (any, error)) {
+	fake.keyMutex.Lock()
+	defer fake.keyMutex.Unlock()
+	fake.KeyStub = stub
+}
+
+func (fake *FakeSigner) KeyArgsForCall(i int) (context.Context, string, string) {
+	fake.keyMutex.RLock()
+	defer fake.keyMutex.RUnlock()
+	argsForCall := fake.keyArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
+}
+
+func (fake *FakeSigner) KeyReturns(result1 any, result2 error) {
+	fake.keyMutex.Lock()
+	defer fake.keyMutex.Unlock()
+	fake.KeyStub = nil
+	fake.keyReturns = struct {
+		result1 any
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeSigner) KeyReturnsOnCall(i int, result1 any, result2 error) {
+	fake.keyMutex.Lock()
+	defer fake.keyMutex.Unlock()
+	fake.KeyStub = nil
+	if fake.keyReturnsOnCall == nil {
+		fake.keyReturnsOnCall = make(map[int]struct {
+			result1 any
+			result2 error
+		})
+	}
+	fake.keyReturnsOnCall[i] = struct {
+		result1 any
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *FakeSigner) Sign(arg1 context.Context, arg2 string, arg3 string, arg4 []byte) ([]byte, error) {
 	var arg4Copy []byte
 	if arg4 != nil {
@@ -104,6 +185,8 @@ func (fake *FakeSigner) SignReturnsOnCall(i int, result1 []byte, result2 error)
 func (fake *FakeSigner) Invocations() map[string][][]interface{} {
 	fake.invocationsMutex.RLock()
 	defer fake.invocationsMutex.RUnlock()
+	fake.keyMutex.RLock()
+	defer fake.keyMutex.RUnlock()
 	fake.signMutex.RLock()
 	defer fake.signMutex.RUnlock()
 	copiedInvocations := map[string][][]interface{}{}
diff --git a/internal/service/policy/policyfakes/fake_storage.go b/internal/service/policy/policyfakes/fake_storage.go
index 1cbab1b9910c438a89835f64ef360762892a4af9..215cabbd5b784342053e28cd0c6147f47a2127b5 100644
--- a/internal/service/policy/policyfakes/fake_storage.go
+++ b/internal/service/policy/policyfakes/fake_storage.go
@@ -102,6 +102,18 @@ type FakeStorage struct {
 		result1 *storage.Policy
 		result2 error
 	}
+	SavePolicyStub        func(context.Context, *storage.Policy) error
+	savePolicyMutex       sync.RWMutex
+	savePolicyArgsForCall []struct {
+		arg1 context.Context
+		arg2 *storage.Policy
+	}
+	savePolicyReturns struct {
+		result1 error
+	}
+	savePolicyReturnsOnCall map[int]struct {
+		result1 error
+	}
 	SetDataStub        func(context.Context, string, map[string]interface{}) error
 	setDataMutex       sync.RWMutex
 	setDataArgsForCall []struct {
@@ -585,6 +597,68 @@ func (fake *FakeStorage) PolicyReturnsOnCall(i int, result1 *storage.Policy, res
 	}{result1, result2}
 }
 
+func (fake *FakeStorage) SavePolicy(arg1 context.Context, arg2 *storage.Policy) error {
+	fake.savePolicyMutex.Lock()
+	ret, specificReturn := fake.savePolicyReturnsOnCall[len(fake.savePolicyArgsForCall)]
+	fake.savePolicyArgsForCall = append(fake.savePolicyArgsForCall, struct {
+		arg1 context.Context
+		arg2 *storage.Policy
+	}{arg1, arg2})
+	stub := fake.SavePolicyStub
+	fakeReturns := fake.savePolicyReturns
+	fake.recordInvocation("SavePolicy", []interface{}{arg1, arg2})
+	fake.savePolicyMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeStorage) SavePolicyCallCount() int {
+	fake.savePolicyMutex.RLock()
+	defer fake.savePolicyMutex.RUnlock()
+	return len(fake.savePolicyArgsForCall)
+}
+
+func (fake *FakeStorage) SavePolicyCalls(stub func(context.Context, *storage.Policy) error) {
+	fake.savePolicyMutex.Lock()
+	defer fake.savePolicyMutex.Unlock()
+	fake.SavePolicyStub = stub
+}
+
+func (fake *FakeStorage) SavePolicyArgsForCall(i int) (context.Context, *storage.Policy) {
+	fake.savePolicyMutex.RLock()
+	defer fake.savePolicyMutex.RUnlock()
+	argsForCall := fake.savePolicyArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeStorage) SavePolicyReturns(result1 error) {
+	fake.savePolicyMutex.Lock()
+	defer fake.savePolicyMutex.Unlock()
+	fake.SavePolicyStub = nil
+	fake.savePolicyReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeStorage) SavePolicyReturnsOnCall(i int, result1 error) {
+	fake.savePolicyMutex.Lock()
+	defer fake.savePolicyMutex.Unlock()
+	fake.SavePolicyStub = nil
+	if fake.savePolicyReturnsOnCall == nil {
+		fake.savePolicyReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.savePolicyReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
 func (fake *FakeStorage) SetData(arg1 context.Context, arg2 string, arg3 map[string]interface{}) error {
 	fake.setDataMutex.Lock()
 	ret, specificReturn := fake.setDataReturnsOnCall[len(fake.setDataArgsForCall)]
@@ -733,6 +807,8 @@ func (fake *FakeStorage) Invocations() map[string][][]interface{} {
 	defer fake.listenPolicyDataChangesMutex.RUnlock()
 	fake.policyMutex.RLock()
 	defer fake.policyMutex.RUnlock()
+	fake.savePolicyMutex.RLock()
+	defer fake.savePolicyMutex.RUnlock()
 	fake.setDataMutex.RLock()
 	defer fake.setDataMutex.RUnlock()
 	fake.setPolicyLockMutex.RLock()
diff --git a/internal/service/policy/service.go b/internal/service/policy/service.go
index 65b85ce5c9c41c02baeb546277a02a244e663055..083df02be50b8131f010675c490059c8b3dc0209 100644
--- a/internal/service/policy/service.go
+++ b/internal/service/policy/service.go
@@ -7,11 +7,10 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"net/http"
 	"strings"
 
 	"github.com/google/uuid"
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/lestrrat-go/jwx/v2/jws"
 	"github.com/open-policy-agent/opa/rego"
 	"github.com/open-policy-agent/opa/storage/inmem"
 	"go.uber.org/zap"
@@ -29,6 +28,11 @@ import (
 //go:generate counterfeiter . RegoCache
 //go:generate counterfeiter . Signer
 
+const (
+	BundleFilename          = "policy_bundle.zip"
+	BundleSignatureFilename = "signature.raw"
+)
+
 type Cache interface {
 	Set(ctx context.Context, key, namespace, scope string, value []byte, ttl int) error
 }
@@ -39,6 +43,7 @@ type RegoCache interface {
 }
 
 type Signer interface {
+	Key(ctx context.Context, namespace string, key string) (any, error)
 	Sign(ctx context.Context, namespace string, key string, data []byte) ([]byte, error)
 }
 
@@ -47,33 +52,26 @@ type Service struct {
 	policyCache RegoCache
 	cache       Cache
 	signer      Signer
+	httpClient  *http.Client
 	logger      *zap.Logger
-}
-
-func New(storage Storage, policyCache RegoCache, cache Cache, signer Signer, logger *zap.Logger) *Service {
-	signerFactory := func(signer Signer) func() (jws.Signer, error) {
-		return func() (jws.Signer, error) {
-			return &signAdapter{signer: signer}, nil
-		}
-	}
 
-	// This unregister/register sequence is done mainly for the unit tests
-	// because each tests creates a new service instance, but the jws.Signer
-	// implementations are kept in a global variable map, which is the same
-	// for all test cases. In order for tests to work and to be able to register
-	// different signer implementations, the previous signer must be explicitly
-	// unregistered.
-	jws.UnregisterSigner(JwaVaultSignature)
-	jwa.UnregisterSignatureAlgorithm(JwaVaultSignature)
-	jwa.RegisterSignatureAlgorithm(JwaVaultSignature)
-	jws.RegisterSigner(JwaVaultSignature, jws.SignerFactoryFn(signerFactory(signer)))
+	// externalHostname specifies the hostname where the policy service can be
+	// reached from the public internet. This setting is very important for
+	// export/import of policy bundles, as the policy service must include the
+	// full path to its verification public key in the bundle, so that verifiers
+	// can use it for signature verification.
+	externalHostname string
+}
 
+func New(storage Storage, policyCache RegoCache, cache Cache, signer Signer, hostname string, httpClient *http.Client, logger *zap.Logger) *Service {
 	return &Service{
-		storage:     storage,
-		policyCache: policyCache,
-		cache:       cache,
-		signer:      signer,
-		logger:      logger,
+		storage:          storage,
+		policyCache:      policyCache,
+		cache:            cache,
+		signer:           signer,
+		httpClient:       httpClient,
+		logger:           logger,
+		externalHostname: hostname,
 	}
 }
 
@@ -259,7 +257,7 @@ func (s *Service) ExportBundle(ctx context.Context, req *policy.ExportBundleRequ
 	// TODO(penkovski): namespace and key must be taken from policy export configuration
 	// This will be implemented with issue #41, for now some test values are hardcoded
 	// https://gitlab.eclipse.org/eclipse/xfsc/tsa/policy/-/issues/41
-	signature, err := s.sign("transit", "key1", bundleDigest[:])
+	signature, err := s.signer.Sign(ctx, "transit", "key1", bundleDigest[:])
 	if err != nil {
 		logger.Error("error signing policy bundle", zap.Error(err))
 		return nil, nil, err
@@ -269,11 +267,11 @@ func (s *Service) ExportBundle(ctx context.Context, req *policy.ExportBundleRequ
 	// zip file and the jws detached payload signature file
 	var files = []ZipFile{
 		{
-			Name:    "policy_bundle.zip",
+			Name:    BundleFilename,
 			Content: bundle,
 		},
 		{
-			Name:    "policy_bundle.jws",
+			Name:    BundleSignatureFilename,
 			Content: signature,
 		},
 	}
@@ -294,6 +292,76 @@ func (s *Service) ExportBundle(ctx context.Context, req *policy.ExportBundleRequ
 	}, io.NopCloser(bytes.NewReader(signedBundle)), nil
 }
 
+// PolicyPublicKey returns the public key in JWK format which must be used to
+// verify a signed policy bundle.
+func (s *Service) PolicyPublicKey(ctx context.Context, req *policy.PolicyPublicKeyRequest) (any, error) {
+	logger := s.logger.With(
+		zap.String("operation", "policyPublicKey"),
+		zap.String("repository", req.Repository),
+		zap.String("group", req.Group),
+		zap.String("name", req.PolicyName),
+		zap.String("version", req.Version),
+	)
+
+	// TODO: get key and namespace from policy export configuration
+	key, err := s.signer.Key(ctx, "transit", "key1")
+	if err != nil {
+		logger.Error("error getting policy public key", zap.Error(err))
+		return nil, err
+	}
+
+	return key, nil
+}
+
+// ImportBundle imports a signed policy bundle.
+func (s *Service) ImportBundle(ctx context.Context, _ *policy.ImportBundlePayload, payload io.ReadCloser) (any, error) {
+	logger := s.logger.With(zap.String("operation", "importBundle"))
+
+	archive, err := io.ReadAll(payload)
+	if err != nil {
+		logger.Error("error reading bundle payload", zap.Error(err))
+		return nil, errors.New(errors.BadRequest, fmt.Errorf("error reading bundle payload: %v", err))
+	}
+
+	files, err := s.unzip(archive)
+	if err != nil {
+		logger.Error("failed to unzip bundle", zap.Error(err))
+		return nil, errors.New(errors.BadRequest, fmt.Errorf("failed to unzip bundle: %v", err))
+	}
+
+	if len(files) != 2 {
+		err := fmt.Errorf("expected to contain two files, but has: %d", len(files))
+		logger.Error("invalid bundle", zap.Error(err))
+		return nil, errors.New(errors.BadRequest, "invalid bundle", err)
+	}
+
+	if err := s.verifyBundle(ctx, files); err != nil {
+		logger.Error("failed to verify bundle", zap.Error(err))
+		return nil, errors.New(errors.Forbidden, "failed to verify bundle", err)
+	}
+	logger.Debug("signature is valid")
+
+	policy, err := s.policyFromBundle(files[0].Content)
+	if err != nil {
+		logger.Error("cannot make policy from bundle", zap.Error(err))
+		return nil, errors.New("cannot make policy from bundle", err)
+	}
+
+	if err := s.storage.SavePolicy(ctx, policy); err != nil {
+		logger.Error("error saving imported policy bundle", zap.Error(err))
+		return nil, errors.New("error saving imported policy bundle", err)
+	}
+
+	return map[string]interface{}{
+		"repository": policy.Repository,
+		"group":      policy.Group,
+		"name":       policy.Name,
+		"version":    policy.Version,
+		"locked":     policy.Locked,
+		"lastUpdate": policy.LastUpdate,
+	}, err
+}
+
 func (s *Service) ListPolicies(ctx context.Context, req *policy.PoliciesRequest) (*policy.PoliciesResult, error) {
 	logger := s.logger.With(zap.String("operation", "listPolicies"))
 
diff --git a/internal/service/policy/service_test.go b/internal/service/policy/service_test.go
index 43c1ff736952b64dba389da54339ed068121367b..e853b80394b3d89a42ba104bfbe90c2c47e54cb2 100644
--- a/internal/service/policy/service_test.go
+++ b/internal/service/policy/service_test.go
@@ -4,7 +4,6 @@ import (
 	"archive/zip"
 	"bytes"
 	"context"
-	"encoding/base64"
 	"fmt"
 	"io"
 	"net/http"
@@ -26,7 +25,7 @@ import (
 )
 
 func TestNew(t *testing.T) {
-	svc := policy.New(nil, nil, nil, nil, zap.NewNop())
+	svc := policy.New(nil, nil, nil, nil, "hostname.com", http.DefaultClient, zap.NewNop())
 	assert.Implements(t, (*goapolicy.Service)(nil), svc)
 }
 
@@ -373,7 +372,7 @@ func TestService_Evaluate(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			svc := policy.New(test.storage, test.regocache, test.cache, nil, zap.NewNop())
+			svc := policy.New(test.storage, test.regocache, test.cache, nil, "hostname.com", http.DefaultClient, zap.NewNop())
 			ctx := context.Background()
 			if test.ctx != nil {
 				ctx = test.ctx
@@ -479,7 +478,7 @@ func TestService_Lock(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			svc := policy.New(test.storage, nil, nil, nil, zap.NewNop())
+			svc := policy.New(test.storage, nil, nil, nil, "hostname.com", http.DefaultClient, zap.NewNop())
 			err := svc.Lock(context.Background(), test.req)
 			if err == nil {
 				assert.Empty(t, test.errtext)
@@ -577,7 +576,7 @@ func TestService_Unlock(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			svc := policy.New(test.storage, nil, nil, nil, zap.NewNop())
+			svc := policy.New(test.storage, nil, nil, nil, "hostname.com", http.DefaultClient, zap.NewNop())
 			err := svc.Unlock(context.Background(), test.req)
 			if err == nil {
 				assert.Empty(t, test.errtext)
@@ -809,7 +808,7 @@ func TestService_ListPolicies(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			svc := policy.New(test.storage, nil, nil, nil, zap.NewNop())
+			svc := policy.New(test.storage, nil, nil, nil, "hostname.com", http.DefaultClient, zap.NewNop())
 			result, err := svc.ListPolicies(context.Background(), test.request)
 
 			if test.errText != "" {
@@ -874,7 +873,7 @@ func TestService_SubscribeForPolicyChange(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			svc := policy.New(test.storage, nil, nil, nil, zap.NewNop())
+			svc := policy.New(test.storage, nil, nil, nil, "hostname.com", http.DefaultClient, zap.NewNop())
 			res, err := svc.SubscribeForPolicyChange(context.Background(), test.request)
 			if test.errText != "" {
 				assert.ErrorContains(t, err, test.errText)
@@ -894,7 +893,7 @@ func TestService_ExportBundleError(t *testing.T) {
 				return nil, errors.New(errors.NotFound, "policy not found")
 			},
 		}
-		svc := policy.New(storage, nil, nil, nil, zap.NewNop())
+		svc := policy.New(storage, nil, nil, nil, "https://policyservice.com", http.DefaultClient, zap.NewNop())
 		res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
 		assert.Nil(t, res)
 		assert.Nil(t, reader)
@@ -911,7 +910,7 @@ func TestService_ExportBundleError(t *testing.T) {
 				return nil, errors.New("unexpected error")
 			},
 		}
-		svc := policy.New(storage, nil, nil, nil, zap.NewNop())
+		svc := policy.New(storage, nil, nil, nil, "https://policyservice.com", http.DefaultClient, zap.NewNop())
 		res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
 		assert.Nil(t, res)
 		assert.Nil(t, reader)
@@ -945,7 +944,7 @@ func TestService_ExportBundleError(t *testing.T) {
 			},
 		}
 
-		svc := policy.New(storage, nil, nil, signer, zap.NewNop())
+		svc := policy.New(storage, nil, nil, signer, "https://policyservice.com", http.DefaultClient, zap.NewNop())
 		res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
 		assert.Nil(t, res)
 		assert.Nil(t, reader)
@@ -977,7 +976,7 @@ func TestService_ExportBundleSuccess(t *testing.T) {
 		},
 	}
 
-	svc := policy.New(storage, nil, nil, signer, zap.NewNop())
+	svc := policy.New(storage, nil, nil, signer, "https://policyservice.com", http.DefaultClient, zap.NewNop())
 	res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
 	require.NoError(t, err)
 	require.NotNil(t, res)
@@ -997,11 +996,11 @@ func TestService_ExportBundleSuccess(t *testing.T) {
 
 	// check if policy_bundle.zip is present
 	require.NotNil(t, r.File[0])
-	require.Equal(t, "policy_bundle.zip", r.File[0].Name)
+	require.Equal(t, policy.BundleFilename, r.File[0].Name)
 
 	// check if policy_bundle.jws is present
 	require.NotNil(t, r.File[1])
-	require.Equal(t, "policy_bundle.jws", r.File[1].Name)
+	require.Equal(t, policy.BundleSignatureFilename, r.File[1].Name)
 
 	// check if signature matches the returned value from signer
 	reader, err = r.File[1].Open()
@@ -1011,11 +1010,5 @@ func TestService_ExportBundleSuccess(t *testing.T) {
 	require.NoError(t, err)
 	require.NotNil(t, sig)
 
-	jwsParts := bytes.Split(sig, []byte("."))
-	assert.Len(t, jwsParts, 3)
-	assert.NotEmpty(t, jwsParts[2])
-
-	s, err := base64.StdEncoding.DecodeString(string(jwsParts[2]))
-	require.NoError(t, err)
-	assert.Equal(t, "signature", string(s))
+	assert.Equal(t, []byte("signature"), sig)
 }
diff --git a/internal/service/policy/storage.go b/internal/service/policy/storage.go
index d22b45eab4fcedb2acc9ff9781d8a2a7e72c1fd8..2a8f0b0cdf5941947674e844916a001c0681b36e 100644
--- a/internal/service/policy/storage.go
+++ b/internal/service/policy/storage.go
@@ -8,6 +8,7 @@ import (
 
 type Storage interface {
 	Policy(ctx context.Context, repository, group, name, version string) (*storage.Policy, error)
+	SavePolicy(ctx context.Context, policy *storage.Policy) error
 	SetPolicyLock(ctx context.Context, repository, group, name, version string, lock bool) error
 	GetPolicies(ctx context.Context, locked *bool) ([]*storage.Policy, error)
 	AddPolicyChangeSubscribers(subscribers ...storage.PolicyChangeSubscriber)
diff --git a/internal/storage/memory/storage.go b/internal/storage/memory/storage.go
index 06df4001cd1e2cdf06391dd127dee75494199e72..fe5c2281e9a46d1d08560731989775fde6cb5f42 100644
--- a/internal/storage/memory/storage.go
+++ b/internal/storage/memory/storage.go
@@ -56,7 +56,31 @@ func (s *Storage) Policy(_ context.Context, repository, group, name, version str
 	return &res, nil
 }
 
-func (s *Storage) SetPolicyLock(_ context.Context, repository, group, name, version string, lock bool) error {
+func (s *Storage) SavePolicy(ctx context.Context, policy *storage.Policy) error {
+	key := s.keyConstructor.ConstructKey(
+		policy.Repository,
+		policy.Group,
+		policy.Name,
+		policy.Version,
+	)
+
+	s.mu.Lock()
+	s.policies[key] = policy
+	s.mu.Unlock()
+
+	// send the changed policy to subscribers
+	go func(policy *storage.Policy) {
+		select {
+		case s.changes <- *policy:
+		case <-time.After(10 * time.Second):
+		case <-ctx.Done():
+		}
+	}(policy)
+
+	return nil
+}
+
+func (s *Storage) SetPolicyLock(ctx context.Context, repository, group, name, version string, lock bool) error {
 	key := s.keyConstructor.ConstructKey(repository, group, name, version)
 
 	s.mu.Lock()
@@ -70,7 +94,11 @@ func (s *Storage) SetPolicyLock(_ context.Context, repository, group, name, vers
 
 	// send the changed policy to subscribers
 	go func(policy *storage.Policy) {
-		s.changes <- *policy
+		select {
+		case s.changes <- *policy:
+		case <-time.After(10 * time.Second):
+		case <-ctx.Done():
+		}
 	}(p)
 
 	return nil
diff --git a/internal/storage/mongodb/storage.go b/internal/storage/mongodb/storage.go
index 354a0a4c94afc36ee469824644a3aa412645e739..56ed1a10588b3752317a40d642b46acfdaa13d7d 100644
--- a/internal/storage/mongodb/storage.go
+++ b/internal/storage/mongodb/storage.go
@@ -80,6 +80,29 @@ func (s *Storage) Policy(ctx context.Context, repository, group, name, version s
 	return &policy, nil
 }
 
+func (s *Storage) SavePolicy(ctx context.Context, policy *storage.Policy) error {
+	opts := options.Update().SetUpsert(true)
+	filter := bson.M{
+		"repository": policy.Repository,
+		"group":      policy.Group,
+		"name":       policy.Name,
+		"version":    policy.Version,
+	}
+	update := bson.M{"$set": bson.M{
+		"locked":              policy.Locked,
+		"rego":                policy.Rego,
+		"data":                policy.Data,
+		"dataConfig":          policy.DataConfig,
+		"outputSchema":        policy.OutputSchema,
+		"lastUpdate":          time.Now(),
+		"nextDataRefreshTime": time.Time{},
+	}}
+
+	_, err := s.policy.UpdateOne(ctx, filter, update, opts)
+
+	return err
+}
+
 func (s *Storage) SetPolicyLock(ctx context.Context, repository, group, name, version string, lock bool) error {
 	_, err := s.policy.UpdateOne(
 		ctx,