Repository: openpubkey/openpubkey
Pull request #55 (2023-11-08 18:13:58)

Creates CONTRIBUTING.md
This is the initial commit of the contributing guidelines. There are no issues or fixes associated with this PR.


Patch:
diff --git a/.gitignore b/.gitignore
index 83ed42b..3b735ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,3 @@
 
 # Go workspace file
 go.work
-
-configs/
-.vscode/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8844ee0
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,102 @@
+# Contributing to OpenPubkey
+
+Welcome to OpenPubkey! We are so excited you are here. Thank you for your interest in contributing your time and expertise to the project. The following document details contribution guidelines.
+
+# Getting Started
+
+Whether you're addressing an open issue (or filing a new one), fixing a typo in our documentation, adding to core capabilities of the project, or introducing a new use case, anyone from the community is welcome here at OpenPubkey.
+
+## Include Licensing at the Top of Each File
+At the top of each file in your commit, please ensure the following is captured in a comment: 
+
+` SPDX-License-Identifier: Apache-2.0 `
+
+## Sign Off on Your Commits
+Contributors are required to sign off on their commits. A sign off certifies that you wrote the associated change or have permission to submit it as an open-source patch. All submissions are bound by the [Developer's Certificate of Origin 1.1](https://developercertificate.org/) and [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
+
+```
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
+```
+
+Your sign off can be added manually to your commit, i.e., `Signed-off-by: Jane Doe <jane.doe@example.com>`. 
+
+Then, you can create a signed off commit using the flag `-s` or `--signoff`:
+`$ git commit -s -m "This is my signed off commit."`.
+
+To verify that your commit was signed off, check your latest log output:
+```
+$ git log -1
+commit <commit id>
+Author: Jane Doe <jane.doe@example.com>
+Date:   Thurs Nov 9 06:14:13 2023 -0400
+
+    This is my signed off commit.
+
+    Signed-off-by: Jane Doe <jane.doe@example.com>
+```
+
+## Pull Request (PR) Process
+
+OpenPubkey is managed from the `main` branch. To ensure your contribution is reviewed, all pull requests must be made against the `main` branch.
+
+PRs must include a brief summary of what the change is, any issues associated with the change, and any fixes the change addresses. Please include the relevant link(s) for any fixed issues. 
+
+Pull requests do not have to pass all automated checks before being opened, but all checks must pass before merging. This can be useful if you need help figuring out why a required check is failing.
+
+Our automated PR checks verify that:
+ 1. All unit tests pass, which can be done locally by running `go test ./...`.
+ 2. The code has been formatted correctly, according to `go fmt`.
+ 3. There are no obvious errors, according to `go vet`.
+
+## Testing OpenPubkey Locally
+
+To build OpenPubkey, ensure you have Go version `>= 1.20` installed. To verify which version you have installed, try `go version`.
+
+To run the [Google example](https://github.com/openpubkey/openpubkey/tree/main/examples/google):
+ 1. Navigate to the `examples/google/` directory. 
+ 2. Execute `go build`
+ 3. Execute `./google login` to generate a valid PK token using Google as your OIDC provider.
+ 4. Execute `./google sign` to use the PK token generated in (3) to sign a verifiable message.
+
+# Contributing Roles
+
+Contributors include anyone in the technical community who contributes code, documentation, or other technical artifacts to the OpenPubkey project.
+
+Committers are Contributors who have earned the ability to modify (“commit”) source code, documentation or other technical artifacts in a project’s repository. Note that Committers are still required to submit pull requests.
+
+A Contributor may become a Committer by a majority approval of the existing Committers. A Committer may be removed by a majority approval of the other existing Committers.
+
+# Current Committers
+
+The Committers of OpenPubkey are:
+1. Ethan Heilman (@EthanHeilman)
+2. Jonny Stoten (@jonnystoten)
+3. Lucie Mugnier (@lgmugnier)
+
+# Copyright
+
+By contributing to this repository, you agree to license your work under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Any work contributed where you are not the original author must display a license header with the original author(s) and source.
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index 1664a97..0000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Security Policy
-
-## Reporting a Vulnerability
-
-**Please do not file a public ticket** mentioning the vulnerability or issue.
-
-To privately report security issues or vulnerabilities send your report to security@bastionzero.com (not for support). A report should include:
-
-- a summary of the issue,
-- the steps needed to reproduce the issue,
-- the potential security impact and, if found, any proposed mitigation.
-
-We do not currently offer bug bounties.
\ No newline at end of file
diff --git a/cert/ca.go b/cert/ca.go
index 54a1bae..4e1a39d 100644
--- a/cert/ca.go
+++ b/cert/ca.go
@@ -8,7 +8,6 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/asn1"
-	"encoding/json"
 	"encoding/pem"
 	"fmt"
 	"math/big"
@@ -59,12 +58,12 @@ func GenCAKeyPair() ([]byte, *ecdsa.PrivateKey, error) {
 }
 
 func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, requiredAudience string) ([]byte, error) {
-	var pkt *pktoken.PKToken
-	if err := json.Unmarshal(pktJson, &pkt); err != nil {
+
+	pkt, err := pktoken.FromJSON(pktJson)
+	if err != nil {
 		return nil, err
 	}
-
-	err := pkt.VerifyCicSig()
+	err = pkt.VerifyCicSig()
 	if err != nil {
 		return nil, err
 	}
@@ -79,17 +78,13 @@ func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, require
 	// 	return nil, err
 	// }
 
-	var payload struct {
-		Issuer   string   `json:"iss"`
-		Audience []string `json:"aud"`
-		Email    string   `json:"email"`
-	}
-	if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
+	iss, aud, email, err := pkt.GetClaims()
+	if err != nil {
 		return nil, err
 	}
 
-	if payload.Audience[0] != requiredAudience {
-		return nil, fmt.Errorf("audience 'aud' claim in PK Token did not match audience required by CA, it was %s instead", payload.Audience)
+	if string(aud) != requiredAudience {
+		return nil, fmt.Errorf("audience 'aud' claim in PK Token did not match audience required by CA, it was %s instead", string(aud))
 	}
 
 	caTemplate, err := x509.ParseCertificate(caBytes)
@@ -97,10 +92,13 @@ func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, require
 		return nil, err
 	}
 
+	subject := string(email)
+	oidcIssuer := iss
+
 	// Based on template from https://github.com/sigstore/fulcio/blob/3c8fbea99c71fedfe47d39e12159286eb443a917/pkg/test/cert_utils.go#L195
 	subTemplate := &x509.Certificate{
 		SerialNumber:   big.NewInt(1),
-		EmailAddresses: []string{payload.Email},
+		EmailAddresses: []string{subject},
 		NotBefore:      time.Now().Add(-1 * time.Minute),
 		NotAfter:       time.Now().Add(time.Hour),
 		KeyUsage:       x509.KeyUsageDigitalSignature,
@@ -110,19 +108,24 @@ func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, require
 			// OID for OIDC Issuer extension
 			Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
 			Critical: false,
-			Value:    []byte(payload.Issuer),
+			Value:    []byte(oidcIssuer),
 		}},
 		SubjectKeyId: []byte(util.Base64EncodeForJWT(pktJson)),
 	}
 
-	cic, err := pkt.GetCicValues()
+	_, _, upkjwk, err := pkt.GetCicValues()
 	if err != nil {
 		return nil, err
 	}
-	upk := cic.PublicKey()
+
+	upk, err := upkjwk.PublicKey()
+	if err != nil {
+		return nil, err
+	}
+	_ = upk
 
 	var rawkey interface{} // This is the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
-	if err := upk.Raw(&rawkey); err != nil {
+	if err := upkjwk.Raw(&rawkey); err != nil {
 		return nil, err
 	}
 	pk := rawkey.(*ecdsa.PublicKey)
@@ -144,4 +147,5 @@ func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, require
 	}
 
 	return pemSubCert.Bytes(), nil
+
 }
diff --git a/client/oidc.go b/client/oidc.go
deleted file mode 100644
index dbf716b..0000000
--- a/client/oidc.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package client
-
-import (
-	"context"
-	"crypto"
-	"crypto/rsa"
-	"encoding/json"
-	"fmt"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/lestrrat-go/jwx/v2/jwk"
-	"github.com/lestrrat-go/jwx/v2/jws"
-	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/util"
-)
-
-const GQSecurityParameter = 256
-
-var ErrNonGQUnsupported = fmt.Errorf("non-GQ signatures are not supported")
-
-// Interface for interacting with the OP (OpenID Provider)
-type OpenIdProvider interface {
-	RequestTokens(ctx context.Context, cicHash string) ([]byte, error)
-	PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey, error)
-	VerifyCICHash(ctx context.Context, idt []byte, expectedCICHash string) error
-	VerifyNonGQSig(ctx context.Context, idt []byte, expectedNonce string) error
-}
-
-func VerifyPKToken(ctx context.Context, pkt *pktoken.PKToken, provider OpenIdProvider, cosPk crypto.PublicKey) error {
-	cic, err := pkt.GetCicValues()
-	if err != nil {
-		return err
-	}
-
-	commitment, err := cic.Hash()
-	if err != nil {
-		return err
-	}
-
-	idt, err := pkt.Compact(pkt.Op)
-	if err != nil {
-		return fmt.Errorf("")
-	}
-
-	sigType, ok := pkt.ProviderSignatureType()
-	if !ok {
-		return fmt.Errorf("provider signature type missing")
-	}
-
-	switch sigType {
-	case pktoken.Gq:
-		// TODO: this needs to get the public key from a log of historic public keys based on the iat time in the token
-		pubKey, err := provider.PublicKey(ctx, idt)
-		if err != nil {
-			return fmt.Errorf("failed to get OP public key: %w", err)
-		}
-
-		rsaPubKey := pubKey.(*rsa.PublicKey)
-
-		err = pkt.VerifyGQSig(rsaPubKey, GQSecurityParameter)
-		if err != nil {
-			return fmt.Errorf("error verifying OP GQ signature on PK Token: %w", err)
-		}
-
-		err = provider.VerifyCICHash(ctx, idt, string(commitment))
-		if err != nil {
-			return fmt.Errorf("failed to verify CIC hash: %w", err)
-		}
-	case pktoken.Oidc:
-		err = provider.VerifyNonGQSig(ctx, idt, string(commitment))
-		if err != nil {
-			if err == ErrNonGQUnsupported {
-				return fmt.Errorf("oidc provider doesn't support non-GQ signatures")
-			}
-			return fmt.Errorf("failed to verify signature from OIDC provider: %w", err)
-		}
-	}
-
-	err = pkt.VerifyCicSig()
-	if err != nil {
-		return fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
-	}
-
-	// Skip Cosigner signature verification if no cosigner pubkey is supplied
-	if cosPk != nil {
-		cosPkJwk, err := jwk.FromRaw(cosPk)
-		if err != nil {
-			return fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
-		}
-
-		err = pkt.VerifyCosSig(cosPkJwk, jwa.KeyAlgorithmFrom("ES256"))
-		if err != nil {
-			return fmt.Errorf("error verify cosigner signature on PK Token: %w", err)
-		}
-	}
-
-	return nil
-}
-
-func ExtractClaim(idt []byte, claimName string) (string, error) {
-	_, payloadB64, _, err := jws.SplitCompact(idt)
-	if err != nil {
-		return "", fmt.Errorf("failed to split/decode JWT: %w", err)
-	}
-
-	payloadJSON, err := util.Base64DecodeForJWT(payloadB64)
-	if err != nil {
-		return "", fmt.Errorf("failed to decode payload")
-	}
-
-	var payload map[string]any
-	err = json.Unmarshal(payloadJSON, &payload)
-	if err != nil {
-		return "", fmt.Errorf("failed to unmarshal payload")
-	}
-
-	claim, ok := payload[claimName]
-	if !ok {
-		return "", fmt.Errorf("claim '%s' missing from payload", claimName)
-	}
-
-	claimStr, ok := claim.(string)
-	if !ok {
-		return "", fmt.Errorf("expected claim '%s' to be a string, was %T", claimName, claim)
-	}
-
-	return claimStr, nil
-}
diff --git a/examples/google/example.go b/examples/google/example.go
index 74fe06a..250d8da 100644
--- a/examples/google/example.go
+++ b/examples/google/example.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"bytes"
-	"context"
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/rand"
@@ -16,9 +15,7 @@ import (
 	"path"
 
 	"github.com/lestrrat-go/jwx/v2/jwa"
-
-	"github.com/openpubkey/openpubkey/client"
-	"github.com/openpubkey/openpubkey/client/providers"
+	"github.com/openpubkey/openpubkey/parties"
 	"github.com/openpubkey/openpubkey/pktoken"
 	"github.com/openpubkey/openpubkey/util"
 	"golang.org/x/crypto/sha3"
@@ -50,7 +47,7 @@ func main() {
 	keyAlgorithm := jwa.ES256
 
 	// Directory for saving data
-	outputDir := "output/google"
+	outputDir := "opk/google"
 
 	command := os.Args[1]
 	switch command {
@@ -80,8 +77,8 @@ func login(outputDir string, alg jwa.KeyAlgorithm, signGQ bool) error {
 		return err
 	}
 
-	client := &client.OpkClient{
-		Op: &providers.GoogleOp{
+	client := &parties.OpkClient{
+		Op: &parties.GoogleOp{
 			ClientID:     clientID,
 			ClientSecret: clientSecret,
 			Issuer:       issuer,
@@ -92,20 +89,27 @@ func login(outputDir string, alg jwa.KeyAlgorithm, signGQ bool) error {
 		},
 	}
 
-	pkt, err := client.OidcAuth(context.Background(), signer, alg, map[string]any{"extra": "yes"}, signGQ)
+	pkt, err := client.OidcAuth(signer, alg, map[string]any{"extra": "yes"}, signGQ)
 	if err != nil {
 		return err
 	}
 
-	// Pretty print our json token
-	pktJson, err := json.MarshalIndent(pkt, "", "  ")
+	pktJson, err := pkt.ToJSON()
 	if err != nil {
 		return err
 	}
-	fmt.Println(string(pktJson))
+
+	// Pretty print our json token
+	var prettyJSON bytes.Buffer
+	if err := json.Indent(&prettyJSON, pktJson, "", "  "); err != nil {
+		return err
+	}
+	fmt.Println(prettyJSON.String())
 
 	// Save our signer and pktoken by writing them to a file
-	return saveLogin(outputDir, signer.(*ecdsa.PrivateKey), pkt)
+	saveLogin(outputDir, signer.(*ecdsa.PrivateKey), pktJson)
+
+	return nil
 }
 
 func sign(message string, outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
@@ -125,7 +129,7 @@ func sign(message string, outputDir string, alg jwa.KeyAlgorithm, signGq bool) e
 	fmt.Println("Hash:", hex.EncodeToString(msgHashSum[:]))
 	fmt.Println("Cert:")
 
-	pktJson, err := json.Marshal(pkt)
+	pktJson, err := pkt.ToJSON()
 	if err != nil {
 		return err
 	}
@@ -141,8 +145,8 @@ func sign(message string, outputDir string, alg jwa.KeyAlgorithm, signGq bool) e
 }
 
 func googleCert(outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
-	client := &client.OpkClient{
-		Op: &providers.GoogleOp{
+	client := &parties.OpkClient{
+		Op: &parties.GoogleOp{
 			ClientID:     clientID,
 			ClientSecret: clientSecret,
 			Issuer:       issuer,
@@ -174,8 +178,8 @@ func googleCert(outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
 		return fmt.Errorf("malformatted skid: %w", err)
 	}
 
-	var skidpkt *pktoken.PKToken
-	if err := json.Unmarshal(skidDecoded, skidpkt); err != nil {
+	skidpkt, err := pktoken.FromJSON(skidDecoded)
+	if err != nil {
 		return fmt.Errorf("failed to extract PK Token from x509 cert: %w", err)
 	}
 
@@ -190,21 +194,13 @@ func googleCert(outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
 	return nil
 }
 
-func saveLogin(outputDir string, sk *ecdsa.PrivateKey, pkt *pktoken.PKToken) error {
-	if err := os.MkdirAll(outputDir, 0777); err != nil {
-		return err
-	}
-
+func saveLogin(outputDir string, sk *ecdsa.PrivateKey, pktJson []byte) error {
 	skFilePath := path.Join(outputDir, skFileName)
 	if err := util.WriteSKFile(skFilePath, sk); err != nil {
 		return err
 	}
 
 	pktFilePath := path.Join(outputDir, pktFileName)
-	pktJson, err := json.Marshal(pkt)
-	if err != nil {
-		return err
-	}
 	return os.WriteFile(pktFilePath, pktJson, 0600)
 }
 
@@ -221,8 +217,8 @@ func loadLogin(outputDir string) (crypto.Signer, *pktoken.PKToken, error) {
 		return nil, nil, err
 	}
 
-	var pkt *pktoken.PKToken
-	if err := json.Unmarshal(pktJson, &pkt); err != nil {
+	pkt, err := pktoken.FromJSON(pktJson)
+	if err != nil {
 		return nil, nil, err
 	}
 
diff --git a/examples/ssh/opkssh/cli.go b/examples/ssh/opkssh/cli.go
deleted file mode 100644
index 47451fc..0000000
--- a/examples/ssh/opkssh/cli.go
+++ /dev/null
@@ -1,252 +0,0 @@
-package main
-
-import (
-	"context"
-	"crypto"
-	"encoding/pem"
-	"errors"
-	"fmt"
-	"path/filepath"
-	"strings"
-
-	"os"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/openpubkey/openpubkey/client"
-	"github.com/openpubkey/openpubkey/client/providers"
-	"github.com/openpubkey/openpubkey/examples/ssh/sshcert"
-	"github.com/openpubkey/openpubkey/util"
-	"golang.org/x/crypto/ssh"
-)
-
-var (
-	key              = []byte("NotASecureKey123")
-	clientID         = "184968138938-g1fddl5tglo7mnlbdak8hbsqhhf79f32.apps.googleusercontent.com"
-	requiredAudience = "184968138938-g1fddl5tglo7mnlbdak8hbsqhhf79f32.apps.googleusercontent.com"
-	// The clientSecret was intentionally checked in for the purposes of this example,. It holds no power. Do not report as a security issue
-	clientSecret = "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" // Google requires a ClientSecret even if this a public OIDC App
-	issuer       = "https://accounts.google.com"
-	scopes       = []string{"openid profile email"}
-	redirURIPort = "3000"
-	callbackPath = "/login-callback"
-	redirectURI  = fmt.Sprintf("http://localhost:%v%v", redirURIPort, callbackPath)
-)
-
-// This code is currently intended as an example for how OpenPubkey can secure SSH access.
-func main() {
-	if len(os.Args) < 2 {
-		fmt.Printf("Example SSH key generator using OpenPubkey: command choices are: login, ver")
-		return
-	}
-	command := os.Args[1]
-
-	op := providers.GoogleOp{
-		ClientID:     clientID,
-		ClientSecret: clientSecret,
-		Issuer:       issuer,
-		Scopes:       scopes,
-		RedirURIPort: redirURIPort,
-		CallbackPath: callbackPath,
-		RedirectURI:  redirectURI,
-	}
-
-	switch command {
-	case "login":
-		{
-			if len(os.Args) != 2 {
-				fmt.Println("Invalid number of arguments for login, should be `opkssh login`")
-				os.Exit(1)
-			}
-
-			// If principals is empty the server does not enforce any principal.
-			// The OPK verifier should use policy to make this decision.
-			principals := []string{}
-
-			gqFalse := false
-			alg := jwa.ES256
-
-			signer, err := util.GenKeyPair(alg)
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-
-			client := &client.OpkClient{
-				Op: &op,
-			}
-
-			certBytes, seckeySshPem, err := createSSHCert(context.Background(), client, signer, alg, gqFalse, principals)
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-
-			// Write ssh secret key and public key to filesystem
-			err = writeKeysToSSHDir(seckeySshPem, certBytes)
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			os.Exit(0)
-		}
-	case "ver":
-		{
-			log(strings.Join(os.Args, " "))
-			policyEnforcer := simpleFilePolicyEnforcer{
-				PolicyFilePath: "/etc/opk/policy",
-			}
-
-			if len(os.Args) != 5 {
-				fmt.Println("Invalid number of arguments for ver, should be `opkssh ver <User (TOKEN u)> <Key type (TOKEN t)> <Cert (TOKEN k)> `")
-				os.Exit(1)
-			}
-			userArg := os.Args[2]
-			certB64Arg := os.Args[3]
-			typArg := os.Args[4]
-
-			authKey, err := authorizedKeysCommand(userArg, typArg, certB64Arg, policyEnforcer.checkPolicy, &op)
-			if err != nil {
-				log(fmt.Sprint(err))
-				os.Exit(1)
-			} else {
-				fmt.Println(authKey)
-				os.Exit(0)
-			}
-		}
-	default:
-		fmt.Println("Error! Unrecognized command:", command)
-	}
-}
-
-// This function is called by the SSH server as the authorizedKeysCommand:
-//
-// The following lines are added to /etc/ssh/sshd_config:
-//
-//	authorizedKeysCommand /etc/opk/opkssh ver %u %t %k
-//	AuthorizedPrincipalsCommandUser root
-//
-// The parameters specified in the config map the parameters sent to the function below.
-// We prepend "Arg" to specify which ones are arguments sent by sshd. They are:
-//
-//	%u The username (requested principal) - userArg
-//	%t The public key type - typArg - in this case a certificate being used as a public key
-//	%k The base64-encoded public key for authentication - certB64Arg - the public key is also a certificate
-func authorizedKeysCommand(userArg string, typArg string, certB64Arg string, policyEnforcer policyCheck, op client.OpenIdProvider) (string, error) {
-	cert, err := sshcert.NewFromAuthorizedKey(typArg, certB64Arg)
-	if err != nil {
-		return "", err
-	}
-	if pkt, err := cert.VerifySshPktCert(op); err != nil {
-		return "", err
-	} else if err := policyEnforcer(userArg, pkt); err != nil {
-		return "", err
-	} else {
-		// sshd expects the public key in the cert, not the cert itself.
-		// This public key is key of the CA the signs the cert, in our
-		// setting there is no CA.
-		pubkeyBytes := ssh.MarshalAuthorizedKey(cert.SshCert.SignatureKey)
-		return "cert-authority " + string(pubkeyBytes), nil
-	}
-}
-
-func createSSHCert(cxt context.Context, client *client.OpkClient, signer crypto.Signer, alg jwa.KeyAlgorithm, gqFlag bool, principals []string) ([]byte, []byte, error) {
-	pkt, err := client.OidcAuth(cxt, signer, alg, map[string]any{}, gqFlag)
-	cert, err := sshcert.New(pkt, principals)
-	if err != nil {
-		return nil, nil, err
-	}
-	sshSigner, err := ssh.NewSignerFromSigner(signer)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	signerMas, err := ssh.NewSignerWithAlgorithms(sshSigner.(ssh.AlgorithmSigner), []string{ssh.KeyAlgoECDSA256})
-	if err != nil {
-		return nil, nil, err
-	}
-
-	sshCert, err := cert.SignCert(signerMas)
-	if err != nil {
-		return nil, nil, err
-	}
-	certBytes := ssh.MarshalAuthorizedKey(sshCert)
-
-	seckeySsh, err := ssh.MarshalPrivateKey(signer, "openpubkey cert")
-	if err != nil {
-		return nil, nil, err
-	}
-	seckeySshBytes := pem.EncodeToMemory(seckeySsh)
-
-	return certBytes, seckeySshBytes, nil
-}
-
-func writeKeys(seckeyPath string, pubkeyPath string, seckeySshPem []byte, certBytes []byte) error {
-	// Write ssh secret key to filesystem
-	if err := os.WriteFile(seckeyPath, seckeySshPem, 0600); err != nil {
-		return err
-	}
-
-	certBytes = append(certBytes, []byte(" "+"openpubkey")...)
-	// Write ssh public key (certificate) to filesystem
-	return os.WriteFile(pubkeyPath, certBytes, 0777)
-}
-
-func fileExists(fPath string) bool {
-	_, err := os.Open(fPath)
-	return !errors.Is(err, os.ErrNotExist)
-}
-
-func writeKeysToSSHDir(seckeySshPem []byte, certBytes []byte) error {
-	homePath, err := os.UserHomeDir()
-	if err != nil {
-		return err
-	}
-	sshPath := filepath.Join(homePath, ".ssh")
-
-	//  To enable ssh to automatically find the key created by openpubkey we
-	// need to use a default ssh key path. However this means that this
-	// filename might already be in use by the user. To ensure we don't
-	// overwrite a ssh key not created by openpubkey we check the comment in the
-	// key to see if it was created by openpubkey
-	for _, keyFilename := range []string{"id_ecdsa", "id_dsa"} {
-		seckeyPath := filepath.Join(sshPath, keyFilename)
-		pubkeyPath := seckeyPath + ".pub"
-
-		if !fileExists(seckeyPath) {
-			// If ssh key file does not currently exist, we don't have to worry about overwriting it
-			return writeKeys(seckeyPath, pubkeyPath, seckeySshPem, certBytes)
-		} else if !fileExists(pubkeyPath) {
-			continue
-		} else {
-			// If ssh key does file does exist, check if it is an openpubkey file, if it is then it is safe to overwrite
-			sshPubkey, err := os.ReadFile(pubkeyPath)
-			if err != nil {
-				fmt.Println("Failed to read:", pubkeyPath)
-				continue
-			}
-			sshPubkeySplit := strings.Split(string(sshPubkey), " ")
-			if len(sshPubkeySplit) != 3 {
-				fmt.Println("Failed to parse:", pubkeyPath)
-				continue
-			}
-			// check if pubkey comment to see if it an openpubkey ssh key
-			if strings.Contains(sshPubkeySplit[2], ("openpubkey")) {
-				// safe to overwrite
-				return writeKeys(seckeyPath, pubkeyPath, seckeySshPem, certBytes)
-			}
-		}
-	}
-	return fmt.Errorf("no default ssh key file free for openpubkey")
-}
-
-func log(line string) {
-	f, err := os.OpenFile("/var/log/openpubkey.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0700)
-	if err != nil {
-		fmt.Println("Couldn't write to file")
-	} else {
-		defer f.Close()
-		if _, err = f.WriteString(line + "\n"); err != nil {
-			fmt.Println("Couldn't write to file")
-		}
-	}
-}
diff --git a/examples/ssh/opkssh/policy.go b/examples/ssh/opkssh/policy.go
deleted file mode 100644
index 132ad2a..0000000
--- a/examples/ssh/opkssh/policy.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"io/fs"
-	"os"
-	"strings"
-
-	"github.com/openpubkey/openpubkey/pktoken"
-	"golang.org/x/exp/slices"
-)
-
-type simpleFilePolicyEnforcer struct {
-	PolicyFilePath string
-}
-
-func (p *simpleFilePolicyEnforcer) readPolicyFile() (string, []string, error) {
-	info, err := os.Stat(p.PolicyFilePath)
-	if err != nil {
-		return "", nil, err
-	}
-	mode := info.Mode()
-
-	// Only the owner of this file should be able to write to it
-	if mode.Perm() != fs.FileMode(0600) {
-		return "", nil, fmt.Errorf("policy file has insecure permissions, expected (0600), got (%o)", mode.Perm())
-	}
-
-	content, err := os.ReadFile(p.PolicyFilePath)
-	if err != nil {
-		return "", nil, err
-	}
-	rows := strings.Split(string(content), "\n")
-
-	for _, row := range rows {
-		entries := strings.Fields(row)
-		if len(entries) > 1 {
-			email := entries[0]
-			allowedPrincipals := entries[1:]
-			return email, allowedPrincipals, nil
-		}
-	}
-	return "", nil, fmt.Errorf("policy file contained no policy")
-}
-
-func (p *simpleFilePolicyEnforcer) checkPolicy(principalDesired string, pkt *pktoken.PKToken) error {
-	allowedEmail, allowedPrincipals, err := p.readPolicyFile()
-	if err != nil {
-		return err
-	}
-	var claims struct {
-		Email string `json:"email"`
-	}
-	if err := json.Unmarshal(pkt.Payload, &claims); err != nil {
-		return err
-	}
-	if string(claims.Email) == allowedEmail {
-		if slices.Contains(allowedPrincipals, principalDesired) {
-			// Access granted
-			return nil
-		} else {
-			return fmt.Errorf("no policy to allow %s to assume %s, check policy config in %s", claims.Email, principalDesired, p.PolicyFilePath)
-		}
-	} else {
-		return fmt.Errorf("no policy for email %s, allowed email is %s, check policy config in %s", claims.Email, allowedEmail, p.PolicyFilePath)
-	}
-}
-
-type policyCheck func(userDesired string, pkt *pktoken.PKToken) error
diff --git a/examples/ssh/sshcert/sshcert.go b/examples/ssh/sshcert/sshcert.go
deleted file mode 100644
index dc6c81c..0000000
--- a/examples/ssh/sshcert/sshcert.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package sshcert
-
-import (
-	"context"
-	"crypto/rand"
-	"encoding/json"
-	"fmt"
-
-	// "github.com/openpubkey/openpubkey/parties"
-
-	"github.com/lestrrat-go/jwx/v2/jwk"
-	"github.com/openpubkey/openpubkey/client"
-	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/util"
-	"golang.org/x/crypto/ssh"
-)
-
-type SshCertSmuggler struct {
-	SshCert *ssh.Certificate
-}
-
-func New(pkt *pktoken.PKToken, principals []string) (*SshCertSmuggler, error) {
-
-	// TODO: assumes email exists in ID Token,
-	// this will break for OPs like Azure that do not have email as a claim
-	var claims struct {
-		Email string `json:"email"`
-	}
-	if err := json.Unmarshal(pkt.Payload, &claims); err != nil {
-		return nil, err
-	}
-
-	pubkeySsh, err := sshPubkeyFromPKT(pkt)
-	if err != nil {
-		return nil, err
-	}
-	pktJson, err := json.Marshal(pkt)
-	if err != nil {
-		return nil, err
-	}
-	pktB64 := string(util.Base64EncodeForJWT(pktJson))
-	sshSmuggler := SshCertSmuggler{
-		SshCert: &ssh.Certificate{
-			Key:             pubkeySsh,
-			CertType:        ssh.UserCert,
-			KeyId:           claims.Email,
-			ValidPrincipals: principals,
-			ValidBefore:     ssh.CertTimeInfinity,
-			Permissions: ssh.Permissions{
-				Extensions: map[string]string{
-					"permit-X11-forwarding":   "",
-					"permit-agent-forwarding": "",
-					"permit-port-forwarding":  "",
-					"permit-pty":              "",
-					"permit-user-rc":          "",
-					"openpubkey-pkt":          pktB64,
-				},
-			},
-		},
-	}
-	return &sshSmuggler, nil
-}
-
-func NewFromAuthorizedKey(certType string, certB64 string) (*SshCertSmuggler, error) {
-	if certPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certType + " " + certB64)); err != nil {
-		return nil, err
-	} else {
-		opkcert := &SshCertSmuggler{
-			SshCert: certPubkey.(*ssh.Certificate),
-		}
-		return opkcert, nil
-	}
-}
-
-func (s *SshCertSmuggler) SignCert(signerMas ssh.MultiAlgorithmSigner) (*ssh.Certificate, error) {
-	if err := s.SshCert.SignCert(rand.Reader, signerMas); err != nil {
-		return nil, err
-	}
-	return s.SshCert, nil
-}
-
-func (s *SshCertSmuggler) VerifyCaSig(caPubkey ssh.PublicKey) error {
-	certCopy := *(s.SshCert)
-	certCopy.Signature = nil
-	certBytes := certCopy.Marshal()
-	certBytes = certBytes[:len(certBytes)-4] // Drops signature length bytes (see crypto.ssh.certs.go)
-	return caPubkey.Verify(certBytes, s.SshCert.Signature)
-}
-
-func (s *SshCertSmuggler) GetPKToken() (*pktoken.PKToken, error) {
-	pktB64, ok := s.SshCert.Extensions["openpubkey-pkt"]
-	if !ok {
-		return nil, fmt.Errorf("cert is missing required openpubkey-pkt extension")
-	}
-	pktJson, err := util.Base64DecodeForJWT([]byte(pktB64))
-	if err != nil {
-		return nil, fmt.Errorf("openpubkey-pkt extension in cert failed deserialization: %w", err)
-	}
-	var pkt *pktoken.PKToken
-	if err = json.Unmarshal(pktJson, &pkt); err != nil {
-		return nil, err
-	}
-	return pkt, nil
-}
-
-func (s *SshCertSmuggler) VerifySshPktCert(op client.OpenIdProvider) (*pktoken.PKToken, error) {
-	pkt, err := s.GetPKToken()
-	if err != nil {
-		return nil, fmt.Errorf("openpubkey-pkt extension in cert failed deserialization: %w", err)
-	}
-
-	err = client.VerifyPKToken(context.Background(), pkt, op, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	cic, err := pkt.GetCicValues()
-	if err != nil {
-		return nil, err
-	}
-	upk := cic.PublicKey()
-
-	cryptoCertKey := (s.SshCert.Key.(ssh.CryptoPublicKey)).CryptoPublicKey()
-	jwkCertKey, err := jwk.FromRaw(cryptoCertKey)
-	if err != nil {
-		return nil, err
-	}
-
-	if jwk.Equal(jwkCertKey, upk) {
-		return pkt, nil
-	} else {
-		return nil, fmt.Errorf("public key 'upk' in PK Token does not match public key in certificate")
-	}
-}
-
-func sshPubkeyFromPKT(pkt *pktoken.PKToken) (ssh.PublicKey, error) {
-	cic, err := pkt.GetCicValues()
-	if err != nil {
-		return nil, err
-	}
-	upk := cic.PublicKey()
-
-	var rawkey any
-	if err := upk.Raw(&rawkey); err != nil {
-		return nil, err
-	}
-	return ssh.NewPublicKey(rawkey)
-}
diff --git a/go.mod b/go.mod
index 2eb86c6..500bd56 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,11 @@ module github.com/openpubkey/openpubkey
 go 1.20
 
 require (
-	github.com/google/uuid v1.4.0
-	github.com/lestrrat-go/jwx/v2 v2.0.16
-	github.com/sirupsen/logrus v1.9.3
+	github.com/google/uuid v1.3.0
+	github.com/lestrrat-go/jwx/v2 v2.0.11
+	github.com/sirupsen/logrus v1.9.2
 	github.com/stretchr/testify v1.8.4
-	github.com/zitadel/oidc/v2 v2.11.0
+	github.com/zitadel/oidc/v2 v2.6.1
 	golang.org/x/crypto v0.14.0
 )
 
@@ -19,7 +19,7 @@ require (
 	github.com/gorilla/schema v1.2.0 // indirect
 	github.com/gorilla/securecookie v1.1.1 // indirect
 	github.com/kr/pretty v0.3.1 // indirect
-	github.com/lestrrat-go/blackmagic v1.0.2 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.1 // indirect
 	github.com/lestrrat-go/httpcc v1.0.1 // indirect
 	github.com/lestrrat-go/httprc v1.0.4 // indirect
 	github.com/lestrrat-go/iter v1.0.2 // indirect
@@ -27,13 +27,12 @@ require (
 	github.com/muhlemmer/gu v0.3.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/segmentio/asm v1.2.0 // indirect
-	golang.org/x/exp v0.0.0-20231006140011-7918f672742d
 	golang.org/x/net v0.17.0 // indirect
-	golang.org/x/oauth2 v0.13.0 // indirect
+	golang.org/x/oauth2 v0.8.0 // indirect
 	golang.org/x/sys v0.13.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
-	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/protobuf v1.31.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 3639b8d..9565b19 100644
--- a/go.sum
+++ b/go.sum
@@ -9,15 +9,12 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
 github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
@@ -28,18 +25,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
-github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
+github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
+github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
 github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
 github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
 github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
 github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
 github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
-github.com/lestrrat-go/jwx/v2 v2.0.14 h1:qm02Xd/pIWSv70vVEutr3Zt7QULnnNRpM4N5+8hQBFs=
-github.com/lestrrat-go/jwx/v2 v2.0.14/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE=
-github.com/lestrrat-go/jwx/v2 v2.0.16 h1:TuH3dBkYTy2giQg/9D8f20znS3JtMRuQJ372boS3lWk=
-github.com/lestrrat-go/jwx/v2 v2.0.16/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE=
+github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ=
+github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg=
 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
@@ -55,11 +50,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
 github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -70,14 +62,11 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zitadel/oidc/v2 v2.6.1 h1:NIW4/KPi9Q2Pae6MKEhrSl+DSC5W4574GlboHildroc=
 github.com/zitadel/oidc/v2 v2.6.1/go.mod h1:vsFNrYCj2x0it0pYmIRVZ12HJ1VGaMVyGl7HLqw5p+Y=
-github.com/zitadel/oidc/v2 v2.11.0 h1:Am4/yQr4iiM5bznRgF3FOp+wLdKx2gzSU73uyI9vvBE=
-github.com/zitadel/oidc/v2 v2.11.0/go.mod h1:enFSVBQI6aE0TEB1ntjXs9r6O6DEosxX4uhEBLBVD8o=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@@ -90,8 +79,6 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
 golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
-golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
-golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -110,13 +97,10 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
-golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
@@ -129,14 +113,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
diff --git a/ca/ca.go b/parties/ca.go
similarity index 90%
rename from ca/ca.go
rename to parties/ca.go
index b0be67b..3f2da24 100644
--- a/ca/ca.go
+++ b/parties/ca.go
@@ -1,4 +1,4 @@
-package ca
+package parties
 
 import (
 	"bytes"
@@ -8,7 +8,6 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/asn1"
-	"encoding/json"
 	"encoding/pem"
 	"fmt"
 	"math/big"
@@ -169,12 +168,14 @@ func (a *Ca) Serv() {
 }
 
 func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
-	var pkt *pktoken.PKToken
-	if err := json.Unmarshal(pktCom, pkt); err != nil {
+
+	pkt, err := pktoken.FromJSON(pktCom)
+
+	if err != nil {
 		return nil, err
 	}
-
-	if err := pkt.VerifyCicSig(); err != nil {
+	err = pkt.VerifyCicSig()
+	if err != nil {
 		return nil, err
 	}
 
@@ -188,17 +189,13 @@ func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
 	// 	return nil, err
 	// }
 
-	var payload struct {
-		Issuer   string `json:"iss"`
-		Audience string `json:"aud"`
-		Email    string `json:"email"`
-	}
-	if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
+	iss, aud, email, err := pkt.GetClaims()
+	if err != nil {
 		return nil, err
 	}
 
-	if payload.Audience != requiredAudience {
-		return nil, fmt.Errorf("audience 'aud' claim in PK Token did not match audience required by CA, it was %s instead", payload.Audience)
+	if string(aud) != requiredAudience {
+		return nil, fmt.Errorf("audience 'aud' claim in PK Token did not match audience required by CA, it was %s instead", string(aud))
 	}
 
 	caTemplate, err := x509.ParseCertificate(caBytes)
@@ -206,10 +203,13 @@ func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
 		return nil, err
 	}
 
+	subject := string(email)
+	oidcIssuer := iss
+
 	// Based on template from https://github.com/sigstore/fulcio/blob/3c8fbea99c71fedfe47d39e12159286eb443a917/pkg/test/cert_utils.go#L195
 	subTemplate := &x509.Certificate{
 		SerialNumber:   big.NewInt(1),
-		EmailAddresses: []string{payload.Email},
+		EmailAddresses: []string{subject},
 		NotBefore:      time.Now().Add(-1 * time.Minute),
 		NotAfter:       time.Now().Add(time.Hour),
 		KeyUsage:       x509.KeyUsageDigitalSignature,
@@ -219,7 +219,7 @@ func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
 			// OID for OIDC Issuer extension
 			Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
 			Critical: false,
-			Value:    []byte(payload.Issuer),
+			Value:    []byte(oidcIssuer),
 		}},
 		SubjectKeyId: []byte(pktCom),
 	}
@@ -231,19 +231,24 @@ func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
 
 	// subPkSk.PublicKey()
 
-	cic, err := pkt.GetCicValues()
+	_, _, upkjwk, err := pkt.GetCicValues()
 	if err != nil {
 		return nil, err
 	}
 
-	upk := cic.PublicKey()
+	upk, err := upkjwk.PublicKey()
+	if err != nil {
+		return nil, err
+	}
+	_ = upk
 
 	// fmt.Println("In CA Pkt :"+ pkt.)
 
 	var rawkey interface{} // This is the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
-	if err := upk.Raw(&rawkey); err != nil {
+	if err := upkjwk.Raw(&rawkey); err != nil {
 		return nil, err
 	}
+	// pk := rawkey.(*ecdsa.PublicKey)
 
 	subCertBytes, err := x509.CreateCertificate(rand.Reader, subTemplate, caTemplate, rawkey, a.pksk)
 	if err != nil {
diff --git a/client/providers/github_actions.go b/parties/githubclient.go
similarity index 53%
rename from client/providers/github_actions.go
rename to parties/githubclient.go
index f74d60c..9567ac0 100644
--- a/client/providers/github_actions.go
+++ b/parties/githubclient.go
@@ -1,4 +1,4 @@
-package providers
+package parties
 
 import (
 	"context"
@@ -14,8 +14,10 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jwa"
 	"github.com/lestrrat-go/jwx/v2/jwk"
 	"github.com/lestrrat-go/jwx/v2/jws"
-	"github.com/openpubkey/openpubkey/client"
-	oidcclient "github.com/zitadel/oidc/v2/pkg/client"
+	"github.com/openpubkey/openpubkey/gq"
+	"github.com/openpubkey/openpubkey/pktoken"
+	"github.com/openpubkey/openpubkey/util"
+	"github.com/zitadel/oidc/v2/pkg/client"
 )
 
 const githubIssuer = "https://token.actions.githubusercontent.com"
@@ -25,7 +27,7 @@ type GithubOp struct {
 	tokenRequestAuthToken string
 }
 
-var _ client.OpenIdProvider = (*GithubOp)(nil)
+var _ OpenIdProvider = (*GithubOp)(nil)
 
 func NewGithubOpFromEnvironment() (*GithubOp, error) {
 	tokenURL, err := getEnvVar("ACTIONS_ID_TOKEN_REQUEST_URL")
@@ -71,20 +73,7 @@ func buildTokenURL(rawTokenURL, audience string) (string, error) {
 	return parsedURL.String(), nil
 }
 
-func (g *GithubOp) VerifyCICHash(ctx context.Context, idt []byte, expectedCICHash string) error {
-	cicHash, err := client.ExtractClaim(idt, "aud")
-	if err != nil {
-		return err
-	}
-
-	if cicHash != expectedCICHash {
-		return fmt.Errorf("aud claim doesn't match, got %q, expected %q", cicHash, expectedCICHash)
-	}
-
-	return nil
-}
-
-func (g *GithubOp) PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey, error) {
+func (g *GithubOp) PublicKey(idt []byte) (PublicKey, error) {
 	j, err := jws.Parse(idt)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse JWS: %w", err)
@@ -95,12 +84,12 @@ func (g *GithubOp) PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey,
 		return nil, fmt.Errorf("expected RS256 alg claim, got %s", alg)
 	}
 
-	discConf, err := oidcclient.Discover(githubIssuer, http.DefaultClient)
+	discConf, err := client.Discover(githubIssuer, http.DefaultClient)
 	if err != nil {
 		return nil, fmt.Errorf("failed to call OIDC discovery endpoint: %w", err)
 	}
 
-	jwks, err := jwk.Fetch(ctx, discConf.JwksURI)
+	jwks, err := jwk.Fetch(context.TODO(), discConf.JwksURI)
 	if err != nil {
 		return nil, fmt.Errorf("failed to fetch to JWKS: %w", err)
 	}
@@ -123,13 +112,13 @@ func (g *GithubOp) PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey,
 	return pubKey, err
 }
 
-func (g *GithubOp) RequestTokens(ctx context.Context, cicHash string) ([]byte, error) {
+func (g *GithubOp) RequestTokens(cicHash string) ([]byte, error) {
 	tokenURL, err := buildTokenURL(g.rawTokenRequestURL, cicHash)
 	if err != nil {
 		return nil, err
 	}
 
-	request, err := http.NewRequestWithContext(ctx, "GET", tokenURL, nil)
+	request, err := http.NewRequest("GET", tokenURL, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -160,6 +149,77 @@ func (g *GithubOp) RequestTokens(ctx context.Context, cicHash string) ([]byte, e
 	return []byte(jwt.Value), err
 }
 
-func (*GithubOp) VerifyNonGQSig(context.Context, []byte, string) error {
-	return client.ErrNonGQUnsupported
+func (g *GithubOp) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (map[string]any, error) {
+	if !pkt.OpSigGQ {
+		return nil, fmt.Errorf("non-GQ signatures not supported for github")
+	}
+
+	cicphJSON, err := util.Base64DecodeForJWT(pkt.CicPH)
+	if err != nil {
+		return nil, err
+	}
+
+	expectedAud := string(util.B64SHA3_256(cicphJSON))
+
+	idt := pkt.OpJWSCompact()
+
+	// TODO: this needs to get the public key from a log of historic public keys based on the iat time in the token
+	pubKey, err := g.PublicKey(idt)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get OP public key: %w", err)
+	}
+
+	rsaPubKey := pubKey.(*rsa.PublicKey)
+
+	sv := gq.NewSignerVerifier(rsaPubKey, gqSecurityParameter)
+	ok := sv.VerifyJWT(idt)
+	if !ok {
+		return nil, fmt.Errorf("error verifying OP GQ signature on PK Token (ID Token invalid)")
+	}
+
+	_, payloadB64, _, err := jws.SplitCompact(idt)
+	if err != nil {
+		return nil, err
+	}
+
+	payloadJSON, err := util.Base64DecodeForJWT(payloadB64)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode payload")
+	}
+
+	var payload map[string]any
+	err = json.Unmarshal(payloadJSON, &payload)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshal payload")
+	}
+	aud := payload["aud"]
+	if aud != expectedAud {
+		return nil, fmt.Errorf("aud doesn't match, got %q, expected %q", aud, expectedAud)
+	}
+
+	err = pkt.VerifyCicSig()
+	if err != nil {
+		return nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+	}
+
+	// Skip Cosigner signature verification if no cosigner pubkey is supplied
+	if cosPk != nil {
+		cosPkJwk, err := jwk.FromRaw(cosPk)
+		if err != nil {
+			return nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+		}
+
+		err = pkt.VerifyCosSig(cosPkJwk, jwa.KeyAlgorithmFrom("ES256"))
+		if err != nil {
+			return nil, fmt.Errorf("error verify cosigner signature on PK Token: %w", err)
+		}
+	}
+
+	cicPH := make(map[string]any)
+	err = json.Unmarshal(cicphJSON, &cicPH)
+	if err != nil {
+		return nil, fmt.Errorf("error unmarshalling CIC: %w", err)
+	}
+
+	return cicPH, nil
 }
diff --git a/client/providers/google.go b/parties/googleclient.go
similarity index 53%
rename from client/providers/google.go
rename to parties/googleclient.go
index 5df83fa..5bfb3ed 100644
--- a/client/providers/google.go
+++ b/parties/googleclient.go
@@ -1,9 +1,10 @@
-package providers
+package parties
 
 import (
 	"context"
 	"crypto"
 	"crypto/rsa"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"os/exec"
@@ -12,15 +13,19 @@ import (
 	"time"
 
 	"github.com/google/uuid"
+	"github.com/lestrrat-go/jwx/v2/jwa"
 	"github.com/lestrrat-go/jwx/v2/jwk"
 	"github.com/lestrrat-go/jwx/v2/jws"
-	"github.com/openpubkey/openpubkey/client"
 	"github.com/sirupsen/logrus"
-	oidcclient "github.com/zitadel/oidc/v2/pkg/client"
+	"github.com/zitadel/oidc/v2/pkg/client"
 	"github.com/zitadel/oidc/v2/pkg/client/rp"
 	"github.com/zitadel/oidc/v2/pkg/oidc"
 
 	httphelper "github.com/zitadel/oidc/v2/pkg/http"
+
+	"github.com/openpubkey/openpubkey/gq"
+	"github.com/openpubkey/openpubkey/pktoken"
+	"github.com/openpubkey/openpubkey/util"
 )
 
 var (
@@ -38,9 +43,9 @@ type GoogleOp struct {
 	server       *http.Server
 }
 
-var _ client.OpenIdProvider = (*GoogleOp)(nil)
+var _ OpenIdProvider = (*GoogleOp)(nil)
 
-func (g *GoogleOp) RequestTokens(ctx context.Context, cicHash string) ([]byte, error) {
+func (g *GoogleOp) RequestTokens(cicHash string) ([]byte, error) {
 	cookieHandler :=
 		httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
 	options := []rp.Option{
@@ -97,7 +102,7 @@ func (g *GoogleOp) RequestTokens(ctx context.Context, cicHash string) ([]byte, e
 		}
 	}()
 
-	defer g.server.Shutdown(ctx)
+	defer g.server.Shutdown(context.TODO())
 
 	select {
 	case err := <-chErr:
@@ -107,32 +112,101 @@ func (g *GoogleOp) RequestTokens(ctx context.Context, cicHash string) ([]byte, e
 	}
 }
 
-func (g *GoogleOp) VerifyCICHash(ctx context.Context, idt []byte, expectedCICHash string) error {
-	cicHash, err := client.ExtractClaim(idt, "nonce")
+func (g *GoogleOp) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (map[string]any, error) {
+	cicphJSON, err := util.Base64DecodeForJWT(pkt.CicPH)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	if cicHash != expectedCICHash {
-		return fmt.Errorf("nonce claim doesn't match, got %q, expected %q", cicHash, expectedCICHash)
+	nonce := string(util.B64SHA3_256(cicphJSON))
+
+	idt := pkt.OpJWSCompact()
+	if pkt.OpSigGQ {
+		// TODO: this needs to get the public key from a log of historic public keys based on the iat time in the token
+		pubKey, err := g.PublicKey(idt)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get OP public key: %w", err)
+		}
+		sv := gq.NewSignerVerifier(pubKey.(*rsa.PublicKey), gqSecurityParameter)
+		ok := sv.VerifyJWT(idt)
+		if !ok {
+			return nil, fmt.Errorf("error verifying OP GQ signature on PK Token (ID Token invalid)")
+		}
+
+		_, payloadB64, _, err := jws.SplitCompact(idt)
+		if err != nil {
+			return nil, err
+		}
+		payloadJSON, err := util.Base64DecodeForJWT(payloadB64)
+		if err != nil {
+			return nil, fmt.Errorf("failed to decode header: %w", err)
+		}
+
+		var payload map[string]any
+		json.Unmarshal(payloadJSON, &payload)
+		if payload["nonce"] != nonce {
+			return nil, fmt.Errorf("nonce doesn't match")
+		}
+
+	} else {
+		options := []rp.Option{
+			rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second), rp.WithNonce(func(ctx context.Context) string { return nonce })),
+		}
+
+		googleRP, err := rp.NewRelyingPartyOIDC(
+			g.Issuer, g.ClientID, g.ClientSecret, g.RedirectURI, g.Scopes,
+			options...)
+
+		if err != nil {
+			return nil, fmt.Errorf("failed to create RP to verify token: %w", err)
+		}
+
+		_, err = rp.VerifyIDToken[*oidc.IDTokenClaims](context.TODO(), string(idt), googleRP.IDTokenVerifier())
+		if err != nil {
+			return nil, fmt.Errorf("error verifying OP signature on PK Token (ID Token invalid): %w", err)
+		}
 	}
 
-	return nil
+	err = pkt.VerifyCicSig()
+	if err != nil {
+		return nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+	}
+
+	// Skip Cosigner signature verification if no cosigner pubkey is supplied
+	if cosPk != nil {
+		cosPkJwk, err := jwk.FromRaw(cosPk)
+		if err != nil {
+			return nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+		}
+
+		err = pkt.VerifyCosSig(cosPkJwk, jwa.KeyAlgorithmFrom("ES256"))
+		if err != nil {
+			return nil, fmt.Errorf("error verify cosigner signature on PK Token: %w", err)
+		}
+	}
+
+	cicPH := make(map[string]any)
+	err = json.Unmarshal(cicphJSON, &cicPH)
+	if err != nil {
+		return nil, fmt.Errorf("error unmarshalling CIC: %w", err)
+	}
+
+	return cicPH, nil
 }
 
-func (g *GoogleOp) PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey, error) {
+func (g *GoogleOp) PublicKey(idt []byte) (PublicKey, error) {
 	j, err := jws.Parse(idt)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse JWS: %w", err)
 	}
 	kid := j.Signatures()[0].ProtectedHeaders().KeyID()
 
-	discConf, err := oidcclient.Discover(g.Issuer, http.DefaultClient)
+	discConf, err := client.Discover(g.Issuer, http.DefaultClient)
 	if err != nil {
 		return nil, fmt.Errorf("failed to call OIDC discovery endpoint: %w", err)
 	}
 
-	jwks, err := jwk.Fetch(ctx, discConf.JwksURI)
+	jwks, err := jwk.Fetch(context.TODO(), discConf.JwksURI)
 	if err != nil {
 		return nil, fmt.Errorf("failed to fetch to JWKS: %w", err)
 	}
@@ -151,27 +225,6 @@ func (g *GoogleOp) PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey,
 	return pubKey, err
 }
 
-func (g *GoogleOp) VerifyNonGQSig(ctx context.Context, idt []byte, expectedNonce string) error {
-	options := []rp.Option{
-		rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second), rp.WithNonce(func(ctx context.Context) string { return expectedNonce })),
-	}
-
-	googleRP, err := rp.NewRelyingPartyOIDC(
-		g.Issuer, g.ClientID, g.ClientSecret, g.RedirectURI, g.Scopes,
-		options...)
-
-	if err != nil {
-		return fmt.Errorf("failed to create RP to verify token: %w", err)
-	}
-
-	_, err = rp.VerifyIDToken[*oidc.IDTokenClaims](ctx, string(idt), googleRP.IDTokenVerifier())
-	if err != nil {
-		return fmt.Errorf("error verifying OP signature on PK Token (ID Token invalid): %w", err)
-	}
-
-	return nil
-}
-
 // https://stackoverflow.com/questions/39320371/how-start-web-server-to-open-page-in-browser-in-golang
 // open opens the specified URL in the default browser of the user.
 func openUrl(url string) error {
diff --git a/client/providers/mocks.go b/parties/mocks.go
similarity index 65%
rename from client/providers/mocks.go
rename to parties/mocks.go
index b2365ef..d70d6c1 100644
--- a/client/providers/mocks.go
+++ b/parties/mocks.go
@@ -1,13 +1,14 @@
-package providers
+package parties
 
 import (
-	"context"
 	"crypto"
+	"crypto/rsa"
 	"time"
 
 	"github.com/lestrrat-go/jwx/v2/jwa"
 	"github.com/lestrrat-go/jwx/v2/jwt"
 	"github.com/lestrrat-go/jwx/v2/jwt/openid"
+	"github.com/openpubkey/openpubkey/pktoken"
 	"github.com/openpubkey/openpubkey/util"
 )
 
@@ -34,11 +35,10 @@ func NewMockOpenIdProvider() (*MockOpenIdProvider, error) {
 	}, nil
 }
 
-func (m *MockOpenIdProvider) RequestTokens(ctx context.Context, cicHash string) ([]byte, error) {
+func (m *MockOpenIdProvider) RequestTokens(cicHash string) ([]byte, error) {
 	token := openid.New()
 
 	token.Set("nonce", cicHash)
-	token.Set("email", "arthur.aardvark@example.com")
 
 	// Required token payload values for OpenID
 	token.Set(jwt.IssuerKey, issuer)
@@ -56,14 +56,10 @@ func (m *MockOpenIdProvider) RequestTokens(ctx context.Context, cicHash string)
 	return signedToken, nil
 }
 
-func (m *MockOpenIdProvider) PublicKey(ctx context.Context, idt []byte) (crypto.PublicKey, error) {
-	return m.signer.Public(), nil
+func (m *MockOpenIdProvider) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (map[string]any, error) {
+	return nil, nil
 }
 
-func (m *MockOpenIdProvider) VerifyCICHash(ctx context.Context, idt []byte, expectedCICHash string) error {
-	return nil
-}
-
-func (m *MockOpenIdProvider) VerifyNonGQSig(ctx context.Context, idt []byte, expectedNonce string) error {
-	return nil
+func (m *MockOpenIdProvider) PublicKey(idt []byte) (PublicKey, error) {
+	return m.signer.Public().(*rsa.PublicKey), nil
 }
diff --git a/client/client.go b/parties/opkclient.go
similarity index 77%
rename from client/client.go
rename to parties/opkclient.go
index d22d252..98beb09 100644
--- a/client/client.go
+++ b/parties/opkclient.go
@@ -1,13 +1,13 @@
-package client
+package parties
 
 import (
-	"context"
 	"crypto"
 	"crypto/rsa"
 	"fmt"
 
 	"github.com/lestrrat-go/jwx/v2/jwa"
 	"github.com/lestrrat-go/jwx/v2/jwk"
+	"github.com/lestrrat-go/jwx/v2/jws"
 	"github.com/zitadel/oidc/v2/pkg/oidc"
 
 	"github.com/openpubkey/openpubkey/gq"
@@ -15,6 +15,8 @@ import (
 	"github.com/openpubkey/openpubkey/pktoken/clientinstance"
 )
 
+const gqSecurityParameter = 256
+
 // Interface for interacting with the MFA Cosigner (MFACos)
 type MFACos interface {
 	// place holder for MFA Cosigner
@@ -27,7 +29,6 @@ type OpkClient struct {
 }
 
 func (o *OpkClient) OidcAuth(
-	ctx context.Context,
 	signer crypto.Signer,
 	alg jwa.KeyAlgorithm,
 	extraClaims map[string]any,
@@ -47,13 +48,13 @@ func (o *OpkClient) OidcAuth(
 	}
 
 	// Define our OIDC nonce as a commitment to the client instance claims
-	nonce, err := cic.Hash()
+	nonce, err := cic.Commitment()
 	if err != nil {
 		return nil, fmt.Errorf("error getting nonce: %w", err)
 	}
 
 	// Use the commitment nonce to complete the OIDC flow and get an ID token from the provider
-	idToken, err := o.Op.RequestTokens(ctx, string(nonce))
+	idToken, err := o.Op.RequestTokens(nonce)
 	if err != nil {
 		return nil, fmt.Errorf("error requesting ID Token: %w", err)
 	}
@@ -71,23 +72,28 @@ func (o *OpkClient) OidcAuth(
 	}
 
 	if signGQ {
-		opKey, err := o.Op.PublicKey(ctx, idToken)
+		opKey, err := o.Op.PublicKey(idToken)
 		if err != nil {
 			return nil, fmt.Errorf("error getting OP public key: %w", err)
 		}
 		rsaPubKey := opKey.(*rsa.PublicKey)
 
-		sv := gq.NewSignerVerifier(rsaPubKey, GQSecurityParameter)
+		sv := gq.NewSignerVerifier(rsaPubKey, gqSecurityParameter)
 		gqToken, err := sv.SignJWT(idToken)
 		if err != nil {
 			return nil, fmt.Errorf("error creating GQ signature: %w", err)
 		}
+		_, _, gqSig, err := jws.SplitCompact(gqToken)
+		if err != nil {
+			return nil, err
+		}
 
-		pkt.AddSignature(gqToken, pktoken.Gq)
+		pkt.OpSig = gqSig
+		pkt.OpSigGQ = true
 		// TODO: make sure old value of OpSig is fully gone from memory
 	}
 
-	err = VerifyPKToken(ctx, pkt, o.Op, nil)
+	_, err = o.Op.VerifyPKToken(pkt, nil)
 	if err != nil {
 		return nil, fmt.Errorf("error verifying PK Token: %w", err)
 	}
@@ -97,6 +103,17 @@ func (o *OpkClient) OidcAuth(
 
 type TokenCallback func(tokens *oidc.Tokens[*oidc.IDTokenClaims])
 
+type PublicKey interface {
+	Equal(x crypto.PublicKey) bool
+}
+
+// Interface for interacting with the OP (OpenID Provider)
+type OpenIdProvider interface {
+	RequestTokens(cicHash string) ([]byte, error)
+	VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (map[string]any, error)
+	PublicKey(idt []byte) (PublicKey, error)
+}
+
 func (o *OpkClient) RequestCert() ([]byte, error) {
 	return nil, fmt.Errorf("cosigning currently unsupported")
 
diff --git a/pktoken/cic.go b/pktoken/cic.go
deleted file mode 100644
index eebfdcb..0000000
--- a/pktoken/cic.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package pktoken
-
-import (
-	"context"
-
-	"github.com/lestrrat-go/jwx/v2/jws"
-	"github.com/openpubkey/openpubkey/pktoken/clientinstance"
-)
-
-func (p *PKToken) GetCicValues() (*clientinstance.Claims, error) {
-	cicPH, err := p.Cic.ProtectedHeaders().AsMap(context.TODO())
-	if err != nil {
-		return nil, err
-	}
-
-	return clientinstance.ParseClaims(cicPH)
-}
-
-func (p *PKToken) VerifyCicSig() error {
-	message := jws.NewMessage().
-		SetPayload(p.Payload).
-		AppendSignature(p.Cic)
-	token, err := jws.Compact(message)
-	if err != nil {
-		return err
-	}
-
-	cic, err := p.GetCicValues()
-	if err != nil {
-		return err
-	}
-
-	_, err = jws.Verify(token, jws.WithKey(cic.PublicKey().Algorithm(), cic.PublicKey()))
-	return err
-}
diff --git a/pktoken/clientinstance/claims.go b/pktoken/clientinstance/claims.go
index 0641ffb..9e6a97e 100644
--- a/pktoken/clientinstance/claims.go
+++ b/pktoken/clientinstance/claims.go
@@ -15,8 +15,6 @@ import (
 
 // Client Instance Claims, referred also as "cic" in the OpenPubKey paper
 type Claims struct {
-	publicKey jwk.Key
-
 	// Claims are stored in the protected header portion of JWS signature
 	protected map[string]any
 }
@@ -46,56 +44,23 @@ func NewClaims(publicKey jwk.Key, claims map[string]any) (*Claims, error) {
 	claims["rz"] = rand
 
 	return &Claims{
-		publicKey: publicKey,
 		protected: claims,
 	}, nil
 }
 
-func ParseClaims(protected map[string]any) (*Claims, error) {
-	// Get our standard headers and make sure they match up
-	if _, ok := protected["rz"]; !ok {
-		return nil, fmt.Errorf(`missing required "rz" claim`)
-	}
-	upk, ok := protected["upk"]
-	if !ok {
-		return nil, fmt.Errorf(`missing required "upk" claim`)
-	}
-	upkBytes, err := json.Marshal(upk)
-	if err != nil {
-		return nil, err
-	}
-	upkjwk, err := jwk.ParseKey(upkBytes)
+// Returns a hash of all client instance claims which includes a random value
+func (c *Claims) Commitment() (string, error) {
+	buf, err := json.Marshal(c.protected)
 	if err != nil {
-		return nil, err
-	}
-	alg, ok := protected["alg"]
-	if !ok {
-		return nil, fmt.Errorf(`missing required "alg" claim`)
-	} else if alg != upkjwk.Algorithm() {
-		return nil, fmt.Errorf(`provided "alg" value different from algorithm provided in "upk" jwk`)
+		return "", err
 	}
-	return &Claims{
-		publicKey: upkjwk,
-		protected: protected,
-	}, nil
-}
 
-func (c *Claims) PublicKey() jwk.Key {
-	return c.publicKey
-}
-
-func (c *Claims) KeyAlgorithm() jwa.KeyAlgorithm {
-	return c.publicKey.Algorithm()
-}
-
-// Returns a hash of all client instance claims which includes a random value
-func (c *Claims) Hash() ([]byte, error) {
-	buf, err := json.Marshal(c.protected)
+	digest := util.B64SHA3_256(buf)
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 
-	return util.B64SHA3_256(buf), nil
+	return string(digest), nil
 }
 
 // This function signs the payload of the provided token with the protected headers
diff --git a/pktoken/cos.go b/pktoken/cos.go
deleted file mode 100644
index f93bc4f..0000000
--- a/pktoken/cos.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package pktoken
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"time"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/lestrrat-go/jwx/v2/jwk"
-	"github.com/lestrrat-go/jwx/v2/jws"
-)
-
-type CosPHeader struct {
-	Alg       string
-	Jwk       interface{}
-	Kid       string
-	Csid      string
-	Eid       string
-	Auth_time int64
-	Iat       int64
-	Exp       int64
-	Mfa       string
-	Ruri      string
-}
-
-func (p *PKToken) GetCosValues() (*CosPHeader, error) {
-	if p.Cos == nil {
-		return nil, fmt.Errorf("cos signature missing")
-	}
-
-	cosPH, err := p.Cos.ProtectedHeaders().AsMap(context.TODO())
-	if err != nil {
-		return nil, err
-	}
-
-	cosPHBytes, err := json.Marshal(cosPH)
-	if err != nil {
-		return nil, err
-	}
-
-	var hds *CosPHeader
-	if err := json.Unmarshal(cosPHBytes, &hds); err != nil {
-		return nil, err
-	}
-
-	return hds, nil
-}
-
-// TODO: Make this take a cosignerid and JWKS that we trust
-func (p *PKToken) VerifyCosSig(cosPk jwk.Key, alg jwa.KeyAlgorithm) error {
-	if p.Cos == nil {
-		return fmt.Errorf("Failed to verify Cosigner signature as the Cosigner Signature is nil.")
-	}
-
-	message := jws.NewMessage().
-		SetPayload(p.Payload).
-		AppendSignature(p.Cic)
-	cosJwsCom, err := jws.Compact(message)
-	if err != nil {
-		return err
-	}
-
-	_, err = jws.Verify(cosJwsCom, jws.WithKey(alg, cosPk))
-	if err != nil {
-		return err
-	}
-
-	hrs, err := p.GetCosValues()
-	if err != nil {
-		return err
-	}
-
-	// Expiration check
-	if hrs.Exp < time.Now().Unix() {
-		return fmt.Errorf("Cosigner Signature on PK Token is expired by %d seconds.", time.Now().Unix()-hrs.Exp)
-	}
-
-	// Check algorithms match
-	if hrs.Alg != alg.String() {
-		return fmt.Errorf("Algorithm in cosigner protected header, %s, does not match algorithm provided, %s.", hrs.Alg, alg)
-	}
-
-	cosPkBytes, err := json.Marshal(hrs.Jwk)
-	if err != nil {
-		return err
-	}
-	cosPkInPH, err := jwk.ParseKey(cosPkBytes)
-	if err != nil {
-		return err
-	}
-	if cosPkInPH.X509CertThumbprint() != cosPk.X509CertThumbprint() {
-		return fmt.Errorf("JWK of cosigner public key in protected header, %v, does not match JWK public key provided, %v.", cosPkInPH, cosPk)
-	}
-
-	// verified
-	return nil
-}
diff --git a/pktoken/gq.go b/pktoken/gq.go
deleted file mode 100644
index 02cd10e..0000000
--- a/pktoken/gq.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package pktoken
-
-import (
-	"crypto/rsa"
-	"fmt"
-
-	"github.com/openpubkey/openpubkey/gq"
-)
-
-func (p *PKToken) VerifyGQSig(pubKey *rsa.PublicKey, gqSecurityParameter int) error {
-	token, err := p.Compact(p.Op)
-	if err != nil {
-		return err
-	}
-
-	sv := gq.NewSignerVerifier(pubKey, gqSecurityParameter)
-	ok := sv.VerifyJWT(token)
-	if !ok {
-		return fmt.Errorf("error verifying OP GQ signature on PK Token (ID Token invalid)")
-	}
-	return nil
-}
diff --git a/pktoken/mocks/pktoken.go b/pktoken/mocks/pktoken.go
deleted file mode 100644
index c9b9fb8..0000000
--- a/pktoken/mocks/pktoken.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package mocks
-
-import (
-	"context"
-	"crypto"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/lestrrat-go/jwx/v2/jwk"
-	"github.com/openpubkey/openpubkey/client/providers"
-	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/pktoken/clientinstance"
-)
-
-func GenerateMockPKToken(signingKey crypto.Signer, alg jwa.KeyAlgorithm) (*pktoken.PKToken, error) {
-	return GenerateMockPKTokenWithEmail(signingKey, alg, "")
-}
-
-func GenerateMockPKTokenWithEmail(signingKey crypto.Signer, alg jwa.KeyAlgorithm, email string) (*pktoken.PKToken, error) {
-
-	jwkKey, err := jwk.PublicKeyOf(signingKey)
-	if err != nil {
-		return nil, err
-	}
-	jwkKey.Set(jwk.AlgorithmKey, alg)
-
-	if email != "" {
-		jwkKey.Set("email", email)
-	}
-
-	cic, err := clientinstance.NewClaims(jwkKey, map[string]any{})
-	if err != nil {
-		return nil, err
-	}
-
-	// Calculate our nonce from our cic values
-	nonce, err := cic.Hash()
-	if err != nil {
-		return nil, err
-	}
-
-	// Generate mock id token
-	op, err := providers.NewMockOpenIdProvider()
-	if err != nil {
-		return nil, err
-	}
-
-	idToken, err := op.RequestTokens(context.Background(), string(nonce))
-	if err != nil {
-		return nil, err
-	}
-
-	// Sign mock id token payload with cic headers
-	cicToken, err := cic.Sign(signingKey, jwkKey.Algorithm(), idToken)
-	if err != nil {
-		return nil, err
-	}
-
-	// Combine two tokens into a PK Token
-	return pktoken.New(idToken, cicToken)
-}
diff --git a/pktoken/osm.go b/pktoken/osm.go
index 1abca59..2338b2e 100644
--- a/pktoken/osm.go
+++ b/pktoken/osm.go
@@ -8,7 +8,7 @@ import (
 )
 
 func (p *PKToken) NewSignedMessage(content []byte, signer crypto.Signer) ([]byte, error) {
-	cic, err := p.GetCicValues()
+	alg, _, _, err := p.GetCicValues()
 	if err != nil {
 		return nil, err
 	}
@@ -20,10 +20,10 @@ func (p *PKToken) NewSignedMessage(content []byte, signer crypto.Signer) ([]byte
 
 	// Create our headers as defined by section 3.5 of the OpenPubkey paper
 	protected := jws.NewHeaders()
-	if err := protected.Set("alg", cic.PublicKey().Algorithm()); err != nil {
+	if err := protected.Set("alg", alg); err != nil {
 		return nil, err
 	}
-	if err := protected.Set("kid", pktHash); err != nil {
+	if err := protected.Set("kid", string(pktHash)); err != nil {
 		return nil, err
 	}
 	if err := protected.Set("typ", "osm"); err != nil {
@@ -33,7 +33,7 @@ func (p *PKToken) NewSignedMessage(content []byte, signer crypto.Signer) ([]byte
 	return jws.Sign(
 		content,
 		jws.WithKey(
-			cic.PublicKey().Algorithm(),
+			alg,
 			signer,
 			jws.WithProtectedHeaders(protected),
 		),
@@ -41,7 +41,7 @@ func (p *PKToken) NewSignedMessage(content []byte, signer crypto.Signer) ([]byte
 }
 
 func (p *PKToken) VerifySignedMessage(osm []byte) ([]byte, error) {
-	cic, err := p.GetCicValues()
+	alg, _, upk, err := p.GetCicValues()
 	if err != nil {
 		return nil, err
 	}
@@ -67,8 +67,8 @@ func (p *PKToken) VerifySignedMessage(osm []byte) ([]byte, error) {
 	}
 
 	// Verify key algorithm header matches cic
-	if protected.Algorithm() != cic.PublicKey().Algorithm() {
-		return nil, fmt.Errorf(`incorrect "alg" header, expected %s but recieved %s`, cic.PublicKey().Algorithm(), protected.Algorithm())
+	if protected.Algorithm() != alg {
+		return nil, fmt.Errorf(`incorrect "alg" header, expected %s but recieved %s`, alg, protected.Algorithm())
 	}
 
 	// Verify kid header matches hash of pktoken
@@ -86,7 +86,7 @@ func (p *PKToken) VerifySignedMessage(osm []byte) ([]byte, error) {
 		return nil, fmt.Errorf(`incorrect "kid" header, expected %s but recieved %s`, pktHash, kid)
 	}
 
-	_, err = jws.Verify(osm, jws.WithKey(cic.PublicKey().Algorithm(), cic.PublicKey()))
+	_, err = jws.Verify(osm, jws.WithKey(alg, upk))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pktoken/pktoken.go b/pktoken/pktoken.go
index 362270f..727c522 100644
--- a/pktoken/pktoken.go
+++ b/pktoken/pktoken.go
@@ -2,9 +2,13 @@ package pktoken
 
 import (
 	"bytes"
+	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"time"
 
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwk"
 	"github.com/lestrrat-go/jwx/v2/jws"
 
 	"github.com/openpubkey/openpubkey/util"
@@ -12,179 +16,398 @@ import (
 	_ "golang.org/x/crypto/sha3"
 )
 
-const sigTypeHeader = "sig_type"
+const SigTypeHeader = "sig_type"
+const SigTypeOIDC = "oidc"
+const SigTypeGQ = "oidc_gq"
+const SigTypeCIC = "cic"
+const SigTypeCos = "cos"
 
-type SignatureType string
-
-const (
-	Oidc SignatureType = "oidc"
-	Gq   SignatureType = "oidc_gq"
-	Cic  SignatureType = "cic"
-	Cos  SignatureType = "cos"
-)
+type JWS struct {
+	Payload    string        `json:"payload"`
+	Signatures []JWSignature `json:"signatures"`
+}
 
-type Signature = jws.Signature
+type JWSignature struct {
+	Protected string         `json:"protected"`
+	Public    map[string]any `json:"header"`
+	Signature string         `json:"signature"`
+}
 
 type PKToken struct {
-	raw []byte // the original, raw representation of the object
+	Payload []byte
+	OpPH    []byte
+	OpSig   []byte
+	OpSigGQ bool
+	CicPH   []byte
+	CicSig  []byte
+	CosPH   []byte
+	CosSig  []byte
 
-	Payload []byte     // decoded payload
-	Op      *Signature // Provider Signature
-	Cic     *Signature // Client Signature
-	Cos     *Signature // Cosigner Signature
+	raw []byte // the original, raw representation of the object
 }
 
 func New(idToken []byte, cicToken []byte) (*PKToken, error) {
-	pkt := &PKToken{}
-	if err := pkt.AddSignature(idToken, Oidc); err != nil {
+	opPH, opPayload, opSig, err := jws.SplitCompact(idToken)
+	if err != nil {
 		return nil, err
 	}
 
-	if err := pkt.AddSignature(cicToken, Cic); err != nil {
+	cicPH, cicPayload, cicSig, err := jws.SplitCompact(cicToken)
+	if err != nil {
 		return nil, err
 	}
 
-	return pkt, nil
+	// Make sure both signatures were signed over the same payload
+	if string(opPayload) != string(cicPayload) {
+		return nil, fmt.Errorf("the provided id token and cic token do not share the same payload")
+	}
+
+	return &PKToken{
+		Payload: opPayload,
+		OpPH:    opPH,
+		OpSig:   opSig,
+		CicPH:   cicPH,
+		CicSig:  cicSig,
+	}, nil
 }
 
-func (p *PKToken) AddSignature(token []byte, sigType SignatureType) error {
-	message, err := jws.Parse(token)
+func FromJWS(jws *JWS, raw []byte) *PKToken {
+	var cic, op, cos JWSignature
+
+	gq := false
+	hasCos := false
+	for _, sig := range jws.Signatures {
+		switch sig.Public[SigTypeHeader] {
+		case SigTypeOIDC:
+			op = sig
+		case SigTypeGQ:
+			op = sig
+			gq = true
+		case SigTypeCIC:
+			cic = sig
+		case SigTypeCos:
+			cos = sig
+			hasCos = true
+		}
+	}
+
+	tok := &PKToken{
+		Payload: []byte(jws.Payload),
+		OpPH:    []byte(op.Protected),
+		OpSig:   []byte(op.Signature),
+		OpSigGQ: gq,
+		CicPH:   []byte(cic.Protected),
+		CicSig:  []byte(cic.Signature),
+	}
+
+	if hasCos {
+		tok.CosPH = []byte(cos.Protected)
+		tok.CosSig = []byte(cos.Signature)
+	}
+
+	if raw != nil {
+		tok.raw = raw
+	}
+
+	return tok
+}
+
+func (p *PKToken) ToJWS() *JWS {
+	var opSignType string
+	if p.OpSigGQ {
+		opSignType = SigTypeGQ
+	} else {
+		opSignType = SigTypeOIDC
+	}
+
+	opHeaders := map[string]any{
+		SigTypeHeader: opSignType,
+	}
+
+	signatures := []JWSignature{
+		{
+			Public:    map[string]any{SigTypeHeader: SigTypeCIC},
+			Protected: string(p.CicPH),
+			Signature: string(p.CicSig),
+		},
+		{
+			Public:    opHeaders,
+			Protected: string(p.OpPH),
+			Signature: string(p.OpSig),
+		},
+	}
+
+	if p.CosPH != nil {
+		signatures = append(signatures, JWSignature{
+			Public:    map[string]any{SigTypeHeader: SigTypeCos},
+			Protected: string(p.CosPH),
+			Signature: string(p.CosSig),
+		})
+	}
+
+	return &JWS{
+		Payload:    string(p.Payload),
+		Signatures: signatures,
+	}
+}
+
+func FromJSON(in []byte) (*PKToken, error) {
+	jws := new(JWS)
+	err := json.Unmarshal(in, jws)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	// If there is no payload, we set the provided token's payload as current, otherwise
-	// we make sure that the new payload matches current
+	return FromJWS(jws, in), nil
+}
+
+func (p *PKToken) ToJSON() ([]byte, error) {
+	jws := p.ToJWS()
+	jwsJSON, err := json.Marshal(jws)
+	if err != nil {
+		return nil, err
+	}
+	return jwsJSON, nil
+}
+
+func (p *PKToken) OpJWSCompact() []byte {
 	if p.Payload == nil {
-		p.Payload = message.Payload()
-	} else if !bytes.Equal(p.Payload, message.Payload()) {
-		return fmt.Errorf("payload in the GQ token (%s) does not match the existing payload in the PK Token (%s)", p.Payload, message.Payload())
+		panic(fmt.Errorf("Payload can not be nil"))
 	}
 
-	public := jws.NewHeaders()
-	if err := public.Set(sigTypeHeader, string(sigType)); err != nil {
-		return err
+	var buf bytes.Buffer
+	buf.WriteString(string(p.OpPH))
+	buf.WriteByte('.')
+	buf.WriteString(string(p.Payload))
+	buf.WriteByte('.')
+	buf.WriteString(string(p.OpSig))
+
+	jwsCom := buf.Bytes()
+	return jwsCom
+}
+
+func (p *PKToken) CicJWSCompact() []byte {
+	if p.Payload == nil {
+		panic(fmt.Errorf("Payload can not be nil"))
 	}
-	signature := message.Signatures()[0].SetPublicHeaders(public)
 
-	switch sigType {
-	case Oidc, Gq:
-		p.Op = signature
-	case Cic:
-		p.Cic = signature
-	case Cos:
-		p.Cos = signature
-	default:
-		return fmt.Errorf("unrecognized signature type: %s", string(sigType))
+	var buf bytes.Buffer
+	buf.WriteString(string(p.CicPH))
+	buf.WriteByte('.')
+	buf.WriteString(string(p.Payload))
+	buf.WriteByte('.')
+	buf.WriteString(string(p.CicSig))
+
+	jwsCom := buf.Bytes()
+	return jwsCom
+}
+
+func (p *PKToken) CosJWSCompact() []byte {
+	if p.Payload == nil {
+		panic(fmt.Errorf("Payload can not be nil"))
 	}
-	return nil
+
+	var buf bytes.Buffer
+	buf.WriteString(string(p.CosPH))
+	buf.WriteByte('.')
+	buf.WriteString(string(p.Payload))
+	buf.WriteByte('.')
+	buf.WriteString(string(p.CosSig))
+
+	jwsCom := buf.Bytes()
+	return jwsCom
 }
 
-func (p *PKToken) ProviderSignatureType() (SignatureType, bool) {
-	sigType, ok := p.Op.PublicHeaders().Get(sigTypeHeader)
-	if !ok {
-		return "", ok
+func (p *PKToken) GetNonce() ([]byte, error) {
+	decodePayload, err := base64.RawStdEncoding.DecodeString(string(p.Payload))
+	if err != nil {
+		return nil, err
+	}
+
+	var payMap map[string]json.RawMessage
+	err = json.Unmarshal(decodePayload, &payMap)
+	if err != nil {
+		return nil, err
 	}
 
-	return SignatureType(sigType.(string)), true
+	var nonce string
+	err = json.Unmarshal(payMap["nonce"], &nonce)
+	if err != nil {
+		return nil, err
+	}
+
+	return []byte(nonce), nil
 }
 
-func (p *PKToken) Compact(sig *Signature) ([]byte, error) {
-	message := jws.NewMessage().
-		SetPayload(p.Payload).
-		AppendSignature(sig)
-	return jws.Compact(message)
+func (p *PKToken) GetClaims() ([]byte, []byte, []byte, error) {
+	decodePayload, err := base64.RawStdEncoding.DecodeString(string(p.Payload))
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	var payMap map[string]interface{}
+	err = json.Unmarshal(decodePayload, &payMap)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	iss := payMap["iss"].(string)
+	aud := payMap["aud"].(string)
+	email := payMap["email"].(string)
+
+	return []byte(iss), []byte(aud), []byte(email), nil
 }
 
-func (p *PKToken) Hash() (string, error) {
-	/*
-		We set the raw variable when unmarshaling from json (the only current string representation of a
-		PK Token) so when we hash we use the same representation that was given for consistency. When the
-		token being hashed is a new PK Token, we marshal it ourselves. This can introduce some issues based
-		on how different languages format their json strings.
-	*/
-	message := p.raw
-	var err error
-	if message == nil {
-		message, err = json.Marshal(p)
-		if err != nil {
-			return "", err
-		}
+func (p *PKToken) GetCicValues() (jwa.KeyAlgorithm, string, jwk.Key, error) {
+	decodedCicPH, err := base64.RawStdEncoding.DecodeString(string(p.CicPH))
+	if err != nil {
+		return nil, "", nil, err
 	}
 
-	hash := util.B64SHA3_256(message)
-	return string(hash), nil
+	var hds map[string]interface{}
+	json.Unmarshal(decodedCicPH, &hds)
+
+	alg := hds["alg"]
+	rz := hds["rz"]
+	upk := hds["upk"]
+
+	algJwk := jwa.KeyAlgorithmFrom(alg)
+	upkBytes, err := json.Marshal(upk)
+	if err != nil {
+		return nil, "", nil, err
+	}
+
+	upkjwk, err := jwk.ParseKey(upkBytes)
+	if err != nil {
+		return nil, "", nil, err
+	}
+
+	return algJwk, rz.(string), upkjwk, nil
 }
 
-func (p *PKToken) MarshalJSON() ([]byte, error) {
-	message := jws.NewMessage().
-		SetPayload(p.Payload).
-		AppendSignature(p.Op).
-		AppendSignature(p.Cic)
+type CosPHeader struct {
+	Alg       string
+	Jwk       interface{}
+	Kid       string
+	Csid      string
+	Eid       string
+	Auth_time int64
+	Iat       int64
+	Exp       int64
+	Mfa       string
+	Ruri      string
+}
 
-	if p.Cos != nil {
-		message.AppendSignature(p.Cos)
+func (p *PKToken) GetCosValues() (*CosPHeader, error) {
+	decodedCosPH, err := base64.RawStdEncoding.DecodeString(string(p.CosPH))
+	if err != nil {
+		return nil, err
 	}
 
-	return json.Marshal(message)
+	var hds *CosPHeader
+	json.Unmarshal(decodedCosPH, &hds)
+
+	return hds, nil
 }
 
-func (p *PKToken) UnmarshalJSON(data []byte) error {
-	var parsed jws.Message
-	if err := json.Unmarshal(data, &parsed); err != nil {
+// func (p *PKToken) GetPublicKey() ([]byte error){
+// 	p.GetCicValues()
+// }
+
+func (p *PKToken) VerifyCicSig() error {
+	cicJwsCom := p.CicJWSCompact()
+
+	alg, _, upk, err := p.GetCicValues()
+	if err != nil {
 		return err
 	}
 
-	p.Payload = parsed.Payload() // base64 decoded
+	_, err = jws.Verify(cicJwsCom, jws.WithKey(alg, upk))
+	if err != nil {
+		return err
+	}
 
-	opCount := 0
-	cicCount := 0
-	cosCount := 0
-	for _, signature := range parsed.Signatures() {
-		sigHeader, ok := signature.PublicHeaders().Get(sigTypeHeader)
-		if !ok {
-			return fmt.Errorf(`pk token signature is missing required "%s" header as public header`, sigTypeHeader)
-		}
+	// Verified
+	return nil
+}
 
-		sigHeaderString, ok := sigHeader.(string)
-		if !ok {
-			return fmt.Errorf(`provided "%s" is of wrong type, expected string`, sigTypeHeader)
-		}
+func (p *PKToken) AddCosSig(jwsCom []byte) error {
 
-		switch SignatureType(sigHeaderString) {
-		case Oidc:
-			opCount += 1
-			p.Op = signature
-		case Gq:
-			opCount += 1
-			p.Op = signature
-		case Cic:
-			cicCount += 1
-			p.Cic = signature
-		case Cos:
-			cosCount += 1
-			p.Cos = signature
-		default:
-			return fmt.Errorf("unrecognized signature types: %s", sigHeaderString)
-		}
+	cosPH, cosPayload, cosSig, err := jws.SplitCompact(jwsCom)
+	if err != nil {
+		return err
+	}
+	if !bytes.Equal(p.Payload, cosPayload) {
+		return fmt.Errorf("Payload in the Cosigner JWS (%s) does not match the existing payload in the PK Token (%s).", p.Payload, cosPayload)
 	}
 
-	// Do some signature count verifications
-	if opCount == 0 {
-		return fmt.Errorf(`at least one signature of type "oidc" or "oidc_gq" is required`)
-	} else if opCount > 1 {
-		return fmt.Errorf(`only one signature of type "oidc" or "oidc_gq" is allowed, found %d`, opCount)
+	p.CosPH = cosPH
+	p.CosSig = cosSig
+	return nil
+}
+
+// TODO: Make this take a cosignerid and JWKS that we trust
+func (p *PKToken) VerifyCosSig(cosPk jwk.Key, alg jwa.KeyAlgorithm) error {
+	if p.CosPH == nil {
+		return fmt.Errorf("Failed to verify Cosigner signature as Cosigner Protected header is nil.")
+	}
+	if p.CosSig == nil {
+		return fmt.Errorf("Failed to verify Cosigner signature as the Cosigner Signature is nil.")
+	}
+
+	cosJwsCom := p.CosJWSCompact()
+	_, err := jws.Verify(cosJwsCom, jws.WithKey(alg, cosPk))
+	if err != nil {
+		return err
+	}
+
+	hrs, err := p.GetCosValues()
+	if err != nil {
+		return err
+	}
+
+	// Expiration check
+	if hrs.Exp < time.Now().Unix() {
+		return fmt.Errorf("Cosigner Signature on PK Token is expired by %d seconds.", time.Now().Unix()-hrs.Exp)
 	}
 
-	if cicCount == 0 {
-		return fmt.Errorf(`at least one signature of type "cic" is required`)
-	} else if cicCount > 1 {
-		return fmt.Errorf(`only one signature of type "cic" is allowed, found %d`, cicCount)
+	// Check algorithms match
+	if hrs.Alg != alg.String() {
+		return fmt.Errorf("Algorithm in cosigner protected header, %s, does not match algorithm provided, %s.", hrs.Alg, alg)
 	}
 
-	if cosCount > 1 {
-		return fmt.Errorf(`only one signature of type "cos" is allowed, found %d`, cosCount)
+	cosPkBytes, err := json.Marshal(hrs.Jwk)
+	if err != nil {
+		return err
+	}
+	cosPkInPH, err := jwk.ParseKey(cosPkBytes)
+	if err != nil {
+		return err
+	}
+	if cosPkInPH.X509CertThumbprint() != cosPk.X509CertThumbprint() {
+		return fmt.Errorf("JWK of cosigner public key in protected header, %v, does not match JWK public key provided, %v.", cosPkInPH, cosPk)
 	}
 
+	// verified
 	return nil
 }
+
+func (p *PKToken) Hash() ([]byte, error) {
+	/*
+		We set the raw variable when unmarshaling from json (the only current string representation of a
+		PK Token) so when we hash we use the same representation that was given for consistency. When the
+		token being hashed is a new PK Token, we marshal it ourselves. This can introduce some issues based
+		on how different languages format their json strings.
+	*/
+	message := p.raw
+	var err error
+	if message == nil {
+		message, err = p.ToJSON()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	hash := util.B64SHA3_256(message)
+	return hash, nil
+}


Test patch:
diff --git a/cert/ca_test.go b/cert/ca_test.go
index b3c143b..bf5cc7b 100644
--- a/cert/ca_test.go
+++ b/cert/ca_test.go
@@ -14,7 +14,6 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jws"
 
 	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/pktoken/mocks"
 	"github.com/openpubkey/openpubkey/util"
 )
 
@@ -24,23 +23,38 @@ func TestCertCreation(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	userAlg := jwa.ES256
-	userSigningKey, err := util.GenKeyPair(userAlg)
+	pktJson, err := json.Marshal(map[string]any{
+		"payload": "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxODQ5NjgxMzg5MzgtZzFmZGRsNXRnbG83bW5sYmRhazhoYnNxaGhmNzlmMzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIxODQ5NjgxMzg5MzgtZzFmZGRsNXRnbG83bW5sYmRhazhoYnNxaGhmNzlmMzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDQ4NTIwMDI0NDQ3NTQxMzYyNzEiLCJlbWFpbCI6ImFub24uYXV0aG9yLmFhcmR2YXJrQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiVmFGaGtlTE9ITXBxVWQ0RU9ZdW84ZyIsIm5vbmNlIjoiNndXMTExY25BajBlZUxzUGFDOGc5WlVkOXRDS2o1ZGNNZkt6OUZYZUFzYyIsIm5hbWUiOiJBbm9ueW1vdXMgQXV0aG9yIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FBY0hUdGRWR0Zab19aXzNoajY2ZFgzWjBHVklVUktLb2dCcGlKaDduLVhnPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IkFub255bW91cyIsImZhbWlseV9uYW1lIjoiQXV0aG9yIiwibG9jYWxlIjoiZW4iLCJpYXQiOjE2ODUzOTU0MDEsImV4cCI6MTY4NTM5OTAwMX0",
+		"signatures": []any{
+			map[string]any{
+				"protected": "eyJhbGciOiJFUzI1NiIsInJ6IjoiYzllYjhkMDc0MDA0NjJlNTk4MWY2MzU1MWI5NGRjNWVhMGI3MTJkYzUwMjczZjVmYTZjZjBiZmM0ZjU1YmFiMCIsInVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IjZxX3I4TDdHaHlIbm1Oblg2dTFObWpNcE9iN1M4UGNDMGViTmw2SnBNLUEiLCJ5IjoiMVpkX0xUU0ctMHNEQ1FRcFZDUGZxNXZ3dVdxMGx5RVU1QUpqdWRVUHh4cyJ9fQ",
+				"header": map[string]any{
+					"sig_type": "cic",
+				},
+				"signature": "kXDYtLRj_wQ2MDgE7Fab5DcV0JhqZlC55CRDVz6GAxgR9mRcp1jwcIchtkf-TsAakEiAeHxcyu9kzw9ERAnKMA",
+			},
+			map[string]any{
+				"protected": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjYwODNkZDU5ODE2NzNmNjYxZmRlOWRhZTY0NmI2ZjAzODBhMDE0NWMiLCJ0eXAiOiJKV1QifQ",
+				"header": map[string]any{
+					"sig_type": "oidc",
+				},
+				"signature": "oWEeXaAIS5YPjnrRQuPXx8TKcssMP55BEiLrdnQ3TYFRtt9SXlzB_vmT6vUlnCL_JGts8XGVS-5CpXm03Ai27-oo_oVqTXBCp3BJm_ZglaGgGHLHQBg98sbqtfLmm86L4g9EuPj8bqNHBIKeZIgYZuSpW9tVYnwZwbKBMVepjHHnpjwL8OgBaktHZAzDIj7JTEUuskLPbYrdyReNrxXCfip-nb0-rwFqB3_hi_6jCCBBm9I2WN_kB-80gE3LisCM_4MCfu4KwYg71WIDEpMaQumTQN7hZTjuC2y38qKSepeDyQdcZgo8Yxbpjj9OyjrAJ9XvZQSyaQJ0-qKkwPX6Ug",
+			},
+			map[string]any{
+				"protected": "eyJhbGciOiJFUzI1NiIsImF1dGhfdGltZSI6MTY4NTM5NTQwMCwiY3NpZCI6Imh0dHBzOi8vY29zaWduZXIuZXhhbXBsZS5jb20iLCJlaWQiOiIxIiwiZXhwIjoxNjg1NDAyNjAwLCJpYXQiOjE2ODUzOTU0MDAsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ii01VVZVVW15YXh6Yl9UeFZpaHZ5Y2ppLUpyWmZqZ2pYZVBDdnhnQnpIcTAiLCJ5IjoiWkZIZzJYcUZ6Vk40U0V3VXJHTHM0QU8zY25LSUNzZ19hcW5EYnZrcXJ4dyJ9fQ",
+				"header": map[string]any{
+					"sig_type": "cos",
+				},
+				"signature": "P_JwaPD7VHNPq5WQADYu7EjjXoKTrU0xSLvmhDbfqr3R3VpAq2z44_r90Yl5u3zyTxvVQttJCLfXMkyooYNdbGoyC0aZrHqJYYQXGfiPGZ2xxZtP96yaEweyCw8_FI2x_-0Uc6drwnQR7AFCuLUQZZBfsKmXjxXy4X69fNkHFcZgX9cOYDwJsWKUEixSyHrSnhGPvkw0QdYS9l3tYiEtrfW6mYSVcJqKsv-bw32UG_1W4Lgg9lr_0T3xrtwfcJ35o4hntt5bi1jEzA62oXgfeBhskvFTdncCgV5kSc-gDF1-EXqoRue6QrT7qe4MOyVysR9PrXv_DwbbwnSuGeg",
+			},
+		},
+	})
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	mockPkt, err := mocks.GenerateMockPKToken(userSigningKey, userAlg)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	pktJson, err := json.Marshal(mockPkt)
-	if err != nil {
-		t.Fatal(err)
-	}
+	requiredAudience := "184968138938-g1fddl5tglo7mnlbdak8hbsqhhf79f32.apps.googleusercontent.com"
 
-	requiredAudience := "also_me"
 	pemSubCert, err := PktTox509(pktJson, caBytes, caPkSk, requiredAudience)
 	if err != nil {
 		t.Fatal(err)
@@ -55,15 +69,11 @@ func TestCertCreation(t *testing.T) {
 
 	certPubkey := cc.PublicKey.(*ecdsa.PublicKey)
 
-	var pkt *pktoken.PKToken
-	if err := json.Unmarshal(pktJson, &pkt); err != nil {
-		t.Fatal(err)
-	}
-
-	sigma, err := pkt.Compact(pkt.Cic)
+	pkt, err := pktoken.FromJSON(pktJson)
 	if err != nil {
 		t.Fatal(err)
 	}
+	sigma := pkt.CicJWSCompact()
 
 	_, err = jws.Verify(sigma, jws.WithKey(jwa.ES256, certPubkey))
 	if err != nil {
diff --git a/examples/ssh/opkssh/cli_test.go b/examples/ssh/opkssh/cli_test.go
deleted file mode 100644
index e5d2cd0..0000000
--- a/examples/ssh/opkssh/cli_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package main
-
-import (
-	"strings"
-	"testing"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/openpubkey/openpubkey/client/providers"
-	"github.com/openpubkey/openpubkey/examples/ssh/sshcert"
-	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/pktoken/mocks"
-	"github.com/openpubkey/openpubkey/util"
-	"golang.org/x/crypto/ssh"
-)
-
-func AllowAllPolicyEnforcer(userDesired string, pkt *pktoken.PKToken) error {
-	return nil
-}
-
-func TestAuthorizedKeysCommand(t *testing.T) {
-	op, err := providers.NewMockOpenIdProvider()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	principals := []string{"guest", "dev"}
-	alg := jwa.ES256
-
-	signer, err := util.GenKeyPair(alg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	email := "arthur.aardvark@example.com"
-	pkt, err := mocks.GenerateMockPKTokenWithEmail(signer, alg, email)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	cert, err := sshcert.New(pkt, principals)
-	if err != nil {
-		t.Error(err)
-	}
-
-	sshSigner, err := ssh.NewSignerFromSigner(signer)
-	if err != nil {
-		t.Error(err)
-	}
-
-	signerMas, err := ssh.NewSignerWithAlgorithms(sshSigner.(ssh.AlgorithmSigner),
-		[]string{ssh.KeyAlgoECDSA256})
-	if err != nil {
-		t.Error(err)
-	}
-
-	sshCert, err := cert.SignCert(signerMas)
-	if err != nil {
-		t.Error(err)
-	}
-	certTypeAndCertB64 := ssh.MarshalAuthorizedKey(sshCert)
-	typeArg := strings.Split(string(certTypeAndCertB64), " ")[0]
-	certB64Arg := strings.Split(string(certTypeAndCertB64), " ")[1]
-
-	userArg := "user"
-	pubkeyList, err := authorizedKeysCommand(userArg, typeArg, certB64Arg, AllowAllPolicyEnforcer, op)
-	if err != nil {
-		t.Error(err)
-	}
-	expectedPubkeyList := "cert-authority ecdsa-sha2-nistp256"
-	if !strings.Contains(pubkeyList, expectedPubkeyList) {
-		t.Error(err)
-	}
-}
diff --git a/examples/ssh/sshcert/sshcert_test.go b/examples/ssh/sshcert/sshcert_test.go
deleted file mode 100644
index 25cfc98..0000000
--- a/examples/ssh/sshcert/sshcert_test.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package sshcert
-
-import (
-	"crypto/rand"
-	"encoding/json"
-	"fmt"
-	"strings"
-	"testing"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/lestrrat-go/jwx/v2/jwk"
-	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/pktoken/mocks"
-	"github.com/openpubkey/openpubkey/util"
-	"golang.org/x/crypto/ssh"
-)
-
-var (
-	caSecretKey = testkey(
-		`-----BEGIN OPENSSH TEST KEY: DO NOT REPORT-----
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
-NhAAAAAwEAAQAAAQEAzIUpmKvLqHofAXVc/HU4eA9niB3l9mWztelMaa7lB5PSPco+Yw48
-bQgg8l3ehBfe2/aLSQgz2nrE+6E23jgtOav57BK3Zs3QIYqpZL8qvSR5xWSvq5wQc1Df+Q
-rxdAK40vK4tutYzlIvYiaZu0B3TBxOCIwgcsfX6KJYRjwfhWgBg/Im2+eMsklAA9D3w4rD
-kkdArQBnLSC7g2zc/Hi/qGSSDE/g6Y77A0X3Sez+VM5vDzbcer9YhCQVoWVL5s6hFObyqu
-JQqTf4JqhSYNhHujNhsLzG2RsTgQkEjwZbEYGKXkKZnc0w7cTpfq0zKkuvuGyMEnyL/6zv
-LjR5d68cywAAA8D9LxhQ/S8YUAAAAAdzc2gtcnNhAAABAQDMhSmYq8uoeh8BdVz8dTh4D2
-eIHeX2ZbO16UxpruUHk9I9yj5jDjxtCCDyXd6EF97b9otJCDPaesT7oTbeOC05q/nsErdm
-zdAhiqlkvyq9JHnFZK+rnBBzUN/5CvF0ArjS8ri261jOUi9iJpm7QHdMHE4IjCByx9fool
-hGPB+FaAGD8ibb54yySUAD0PfDisOSR0CtAGctILuDbNz8eL+oZJIMT+DpjvsDRfdJ7P5U
-zm8PNtx6v1iEJBWhZUvmzqEU5vKq4lCpN/gmqFJg2Ee6M2GwvMbZGxOBCQSPBlsRgYpeQp
-mdzTDtxOl+rTMqS6+4bIwSfIv/rO8uNHl3rxzLAAAAAwEAAQAAAQBKOOlnprE6a1dlSBp+
-5Guh5rVECNW0HiSiGBDLKdWkclkSY5tQh5IWX6TVUIu4lJEkcs0JrBhlabijOVaYPvrquy
-bwLbqxbG/kPFZNYbM5AUvP/0JhnTm7H9aoovgNig9ZPw0aFT8dYWYg0LFp63NgA8WuBGyi
-OzR4ELLIinlGCFqsR8W8C2E3dgogXqJQvaGg4Q+E9xjpxeiySl9eKQCtnul4kJ8tz7adIl
-ntdTTpi2K1OkIWGt+jjuOFAe33Vq77ub3TxolIPfh+1COx1YJ4dlTSTZTScRIdX5W3bQZn
-681Vi0hqpmtMPkJ7F++38HDJzbd5yaQTcv7m7pXBh7aBAAAAgGyx/CNr3vt+WJKukHu8DZ
-naQ/B3lz4GNaJwed0sMpEKuaLXYoaefJKXVPq6hSimC9ScctzOKCizjQf20Goa96Jju4kt
-Zerw6y9vgufGL9prXVyjuCyHs4sxwKyOew7QuQzpu3ArVGMCgTfZE9tn0Ga6FfcjgKxvuJ
-k+KkoqblEzAAAAgQDnmzWHBeU0oXyMyPt4SeMozqcCkDY6pM+FZspf0zAYfLcrK4Tni74K
-enV8+ZyjNPpfNAWZ6roNZQ4HUz5tLs2OMI4OxG+ptWDHbm3nppYqfg0Qcy7jl1NBBh9XNM
-AwX2CwpoGpqcKWkcnH3/ZmN/8QIoTjl6uv6U0hLwBbVvFyyQAAAIEA4g+hppjyRW+G2WSW
-nCfwQSQ15QL43hQVbPXwZiokEcmaueRjC0s6i/5tjKgnV8eQa9A0BdoxUa67DKCVvthUs/
-mFplwGXA0qGsvlqL9TYCm2wA4VLFzXW9bxvPLqI+0WuB79qmZn4V64PSj6XYYPOGdWHw5k
-uw2Z5widzugx6PMAAAAHdGVzdF9jYQECAwQ=
------END OPENSSH TEST KEY: DO NOT REPORT-----`)
-
-	caPubkey, _, _, _, _ = ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMhSmYq8uoeh8BdVz8dTh4D2eIHeX2ZbO16UxpruUHk9I9yj5jDjxtCCDyXd6EF97b9otJCDPaesT7oTbeOC05q/nsErdmzdAhiqlkvyq9JHnFZK+rnBBzUN/5CvF0ArjS8ri261jOUi9iJpm7QHdMHE4IjCByx9foolhGPB+FaAGD8ibb54yySUAD0PfDisOSR0CtAGctILuDbNz8eL+oZJIMT+DpjvsDRfdJ7P5Uzm8PNtx6v1iEJBWhZUvmzqEU5vKq4lCpN/gmqFJg2Ee6M2GwvMbZGxOBCQSPBlsRgYpeQpmdzTDtxOl+rTMqS6+4bIwSfIv/rO8uNHl3rxzL test_ca"))
-
-	testMsg    = []byte("1234")
-	badTestMsg = []byte("123X")
-)
-
-func testkey(key string) []byte {
-	return []byte(strings.ReplaceAll(key, "TEST KEY: DO NOT REPORT", "PRIVATE KEY"))
-}
-
-func newSshSignerFromPem(pemBytes []byte) (ssh.MultiAlgorithmSigner, error) {
-	rawKey, err := ssh.ParseRawPrivateKey(pemBytes)
-	if err != nil {
-		return nil, err
-	}
-	sshSigner, err := ssh.NewSignerFromKey(rawKey)
-	if err != nil {
-		return nil, err
-	}
-	return ssh.NewSignerWithAlgorithms(sshSigner.(ssh.AlgorithmSigner), []string{ssh.KeyAlgoRSASHA256})
-}
-
-func TestCASignerCreation(t *testing.T) {
-	caSigner, err := newSshSignerFromPem(caSecretKey)
-	if err != nil {
-		t.Error(err)
-	}
-
-	sshSig, err := caSigner.Sign(rand.Reader, testMsg)
-	if err != nil {
-		t.Error(err)
-	}
-	err = caPubkey.Verify(badTestMsg, sshSig)
-	if err == nil {
-		t.Error(fmt.Errorf("expected for signature to fail as the wrong message is used"))
-	}
-}
-
-func TestSshCertCreation(t *testing.T) {
-	caSigner, err := newSshSignerFromPem(caSecretKey)
-	if err != nil {
-		t.Error(err)
-	}
-
-	principals := []string{"guest", "dev"}
-
-	alg := jwa.ES256
-
-	signingKey, err := util.GenKeyPair(alg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	email := "arthur.aardvark@example.com"
-	pkt, err := mocks.GenerateMockPKTokenWithEmail(signingKey, alg, email)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	cert, err := New(pkt, principals)
-	if err != nil {
-		t.Error(err)
-	}
-
-	sshCert, err := cert.SignCert(caSigner)
-	if err != nil {
-		t.Error(err)
-	}
-
-	if err := cert.VerifyCaSig(caPubkey); err != nil {
-		t.Error(err)
-	}
-
-	checker := ssh.CertChecker{}
-	if err = checker.CheckCert("guest", sshCert); err != nil {
-		t.Error(err)
-	}
-
-	if sshCert.KeyId != email {
-		t.Error(fmt.Errorf("expected KeyId to be (%s) but was (%s)", email, sshCert.KeyId))
-	}
-
-	pktB64, ok := sshCert.Extensions["openpubkey-pkt"]
-	if !ok {
-		t.Error(err)
-	}
-	pktExtJson, err := util.Base64DecodeForJWT([]byte(pktB64))
-
-	var pktExt *pktoken.PKToken
-	err = json.Unmarshal(pktExtJson, &pktExt)
-	if err != nil {
-		t.Error(err)
-	}
-
-	cic, err := pktExt.GetCicValues()
-	if err != nil {
-		t.Error(err)
-	}
-	upk := cic.PublicKey()
-
-	cryptoCertKey := (sshCert.Key.(ssh.CryptoPublicKey)).CryptoPublicKey()
-	jwkCertKey, err := jwk.FromRaw(cryptoCertKey)
-	if !jwk.Equal(upk, jwkCertKey) {
-		t.Error(fmt.Errorf("expected upk to be equal to the value in sshCert.Key"))
-	}
-}
diff --git a/gq/gq_test.go b/gq/gq_test.go
index 795c141..5513d29 100644
--- a/gq/gq_test.go
+++ b/gq/gq_test.go
@@ -1,7 +1,6 @@
 package gq_test
 
 import (
-	"bytes"
 	"crypto/rand"
 	"crypto/rsa"
 	"encoding/json"
@@ -11,7 +10,6 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jwa"
 	"github.com/lestrrat-go/jwx/v2/jws"
 	"github.com/openpubkey/openpubkey/gq"
-	"github.com/openpubkey/openpubkey/util"
 )
 
 func TestProveVerify(t *testing.T) {
@@ -35,95 +33,10 @@ func TestProveVerify(t *testing.T) {
 
 	ok := signerVerifier.VerifyJWT(gqToken)
 	if !ok {
-		t.Fatal("signature verification failed")
+		t.Fatal("couldn't verify signature we just made")
 	}
 }
 
-func TestVerifyModifiedIdPayload(t *testing.T) {
-	oidcPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	oidcPubKey := &oidcPrivKey.PublicKey
-
-	idToken, err := createOIDCToken(oidcPrivKey, "test")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// modify the ID Token payload to detect IdP signature invalidity via GQ verify
-	modifiedToken, err := modifyTokenPayload(idToken, "fail")
-	if err != nil {
-		t.Fatal(err)
-	}
-	_, err = jws.Verify(modifiedToken, jws.WithKey(jwa.RS256, oidcPubKey))
-	if err == nil {
-		t.Fatal("ID token signature should fail for modified token")
-	}
-
-	signerVerifier := gq.NewSignerVerifier(oidcPubKey, 256)
-	gqToken, err := signerVerifier.SignJWT(modifiedToken)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	ok := signerVerifier.VerifyJWT(gqToken)
-	if ok {
-		t.Fatal("GQ signature verification passed for invalid payload")
-	}
-}
-
-func TestVerifyModifiedGqPayload(t *testing.T) {
-	oidcPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	oidcPubKey := &oidcPrivKey.PublicKey
-
-	idToken, err := createOIDCToken(oidcPrivKey, "test")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	signerVerifier := gq.NewSignerVerifier(oidcPubKey, 256)
-	gqToken, err := signerVerifier.SignJWT(idToken)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// modify the ID Token payload to detect GQ signature invalidity
-	modifiedToken, err := modifyTokenPayload(gqToken, "fail")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	ok := signerVerifier.VerifyJWT(modifiedToken)
-	if ok {
-		t.Fatal("GQ signature verification passed for invalid payload")
-	}
-}
-
-func modifyTokenPayload(token []byte, audience string) ([]byte, error) {
-	headers, _, signature, err := jws.SplitCompact(token)
-	if err != nil {
-		return nil, err
-	}
-	newPayload := map[string]any{
-		"sub": "1",
-		"iss": "test",
-		"aud": audience,
-		"iat": time.Now().Unix(),
-	}
-	modifiedPayload, err := json.Marshal(newPayload)
-	if err != nil {
-		return nil, err
-	}
-	newToken := bytes.Join([][]byte{headers, util.Base64EncodeForJWT(modifiedPayload), signature}, []byte{'.'})
-	return newToken, nil
-}
-
 func createOIDCToken(oidcPrivKey *rsa.PrivateKey, audience string) ([]byte, error) {
 	alg := jwa.RS256 // RSASSA-PKCS-v1.5 using SHA-256
 
diff --git a/client/opkclient_test.go b/parties/opkclient_test.go
similarity index 57%
rename from client/opkclient_test.go
rename to parties/opkclient_test.go
index 34163b7..7eb4bb7 100644
--- a/client/opkclient_test.go
+++ b/parties/opkclient_test.go
@@ -1,19 +1,16 @@
-package client_test
+package parties
 
 import (
-	"context"
 	"crypto/rsa"
 	"fmt"
 	"testing"
 
 	"github.com/lestrrat-go/jwx/v2/jwa"
-	"github.com/openpubkey/openpubkey/client"
-	"github.com/openpubkey/openpubkey/client/providers"
 	"github.com/openpubkey/openpubkey/gq"
 	"github.com/openpubkey/openpubkey/util"
 )
 
-func TestClient(t *testing.T) {
+func TestSigner(t *testing.T) {
 	alg := jwa.ES256
 
 	testCases := []struct {
@@ -24,7 +21,7 @@ func TestClient(t *testing.T) {
 		{name: "with GQ", gq: true},
 	}
 
-	op, err := providers.NewMockOpenIdProvider()
+	op, err := NewMockOpenIdProvider()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -35,28 +32,24 @@ func TestClient(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		c := client.OpkClient{
+		client := OpkClient{
 			Op: op,
 		}
 
-		pkt, err := c.OidcAuth(context.Background(), signer, alg, map[string]any{}, tc.gq)
+		pkt, nil := client.OidcAuth(signer, alg, map[string]any{}, tc.gq)
 		if err != nil {
 			t.Fatal(err)
 		}
 
 		if tc.gq {
 			// Verify our GQ signature
-			idt, err := pkt.Compact(pkt.Op)
+			idt := pkt.OpJWSCompact()
+			opPubKey, err := op.PublicKey(idt)
 			if err != nil {
 				t.Fatal(err)
 			}
 
-			opPubKey, err := op.PublicKey(context.Background(), idt)
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			sv := gq.NewSignerVerifier(opPubKey.(*rsa.PublicKey), client.GQSecurityParameter)
+			sv := gq.NewSignerVerifier(opPubKey.(*rsa.PublicKey), gqSecurityParameter)
 			ok := sv.VerifyJWT(idt)
 			if !ok {
 				t.Fatal(fmt.Errorf("error verifying OP GQ signature on PK Token (ID Token invalid)"))
diff --git a/pktoken/pktoken_test.go b/pktoken/pktoken_test.go
index 45652ca..d135260 100644
--- a/pktoken/pktoken_test.go
+++ b/pktoken/pktoken_test.go
@@ -2,16 +2,15 @@ package pktoken_test
 
 import (
 	"crypto"
-	"encoding/json"
 	"testing"
 
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwk"
 	"github.com/stretchr/testify/require"
 
+	"github.com/openpubkey/openpubkey/parties"
 	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/pktoken/mocks"
-
-	"github.com/lestrrat-go/jwx/v2/jwa"
-
+	"github.com/openpubkey/openpubkey/pktoken/clientinstance"
 	"github.com/openpubkey/openpubkey/util"
 )
 
@@ -23,7 +22,13 @@ func TestPkToken(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	pkt, err := mocks.GenerateMockPKToken(signingKey, alg)
+	jwkKey, err := jwk.PublicKeyOf(signingKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	jwkKey.Set(jwk.AlgorithmKey, alg)
+
+	pkt, err := generateMockPKToken(signingKey, jwkKey)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -53,20 +58,53 @@ func testPkTokenMessageSigning(t *testing.T, pkt *pktoken.PKToken, signingKey cr
 
 func testPkTokenSerialization(t *testing.T, pkt *pktoken.PKToken) {
 	// Test json serialization/deserialization
-	pktJson, err := json.Marshal(pkt)
+	pktJson, err := pkt.ToJSON()
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	var newPkt *pktoken.PKToken
-	if err := json.Unmarshal(pktJson, &newPkt); err != nil {
+	token, err := pktoken.FromJSON(pktJson)
+	if err != nil {
 		t.Fatal(err)
 	}
 
-	newPktJson, err := json.Marshal(newPkt)
+	tokenJson, err := token.ToJSON()
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	require.JSONEq(t, string(pktJson), string(newPktJson))
+	require.JSONEq(t, string(pktJson), string(tokenJson))
+}
+
+func generateMockPKToken(signingKey crypto.Signer, jwkKey jwk.Key) (*pktoken.PKToken, error) {
+	cic, err := clientinstance.NewClaims(jwkKey, map[string]any{})
+	if err != nil {
+		return nil, err
+	}
+
+	// Calculate our nonce from our cic values
+	nonce, err := cic.Commitment()
+	if err != nil {
+		return nil, err
+	}
+
+	// Generate mock id token
+	op, err := parties.NewMockOpenIdProvider()
+	if err != nil {
+		return nil, err
+	}
+
+	idToken, err := op.RequestTokens(nonce)
+	if err != nil {
+		return nil, err
+	}
+
+	// Sign mock id token payload with cic headers
+	cicToken, err := cic.Sign(signingKey, jwkKey.Algorithm(), idToken)
+	if err != nil {
+		return nil, err
+	}
+
+	// Combine two tokens into a PK Token
+	return pktoken.New(idToken, cicToken)
 }