Repository: openpubkey/openpubkey
Pull request #45 (2023-10-21 14:31:38)

Use jws signatures
The goal of this PR was to improve quality of life a little bit in the PK Token. I noticed we were rolling a lot of our own stuff and for now, I think it makes it simpler and more robust to rely on the existing `jws` library. 

The functionality remains the same, however there were some condensations, removals, and additions. Here's what's changed:

### More JWS Signatures!
The core change of this PR is the new PKToken object, we used to keep track of headers and signatures separately but I think it makes it a lot more flexible to store them as `*jws.Signature`s. 

I created an alias `type Signature = jws.Signature` with the intent that we can always make our own object later but since we're using it as a functional arg, I think it's always best practice not to rely on the object signatures of an underlying library that isn't in go standard.
```golang
type Signature = jws.Signature

type PKToken struct {
	raw []byte // the original, raw representation of the object

	Payload []byte     // decoded payload
	Op      *Signature // Provider Signature
	Cic     *Signature // Client Signature
	Cos     *Signature // Cosigner Signature
}
```

### More Files in Tree
This one might be controversion, but to me the `PKToken` is the powerhouse of our repo. I imagine its gonna get big and so we should try to keep it as organized as possible. What made the most sense to me is to create different files, each for a type of signature: `cos.go`, `cic.go`, and `gq.go` which hold the functions for signing and verifying each type of signature plus any constants or structs that might only apply to each. This frees up our main `pktoken.go` to only contain general functions. This might change in the future but since the goal was trying to improve QOL here, it made it significantly easier to find everything once I'd made this split and I think it lends itself well to the current architecture and functionality of our `PKToken`.
```
└── pktoken
    ├── cic.go
    ├── cos.go
    ├── gq.go
    ├── osm.go
    ├── pktoken.go
    └── pktoken_test.go
```

### New Signature Type
I collected our previous constants into a `SignatureType`. This will help us keep it nice and tight.
```golang
type SignatureType string

const (
	Oidc SignatureType = "oidc"
	Gq   SignatureType = "oidc_gq"
	Cic  SignatureType = "cic"
	Cos  SignatureType = "cos"
)
```

### Custom JSON Marshaler and UnMarshaler

The biggest change is in how we're converting to and from JSON. Before we had a few functions with supporting object etc.. I thought this would be a perfect place to rely on the `jws` library and some go magic. I've created a custom marshaler: `func (p *PKToken) MarshalJSON() ([]byte, error)`, and unmarshaler: `func (p *PKToken) UnmarshalJSON(data []byte) error`, for our object so that when we make regular json calls, the appropriate one will be used.
```golang
var pkt *pktoken.PKToken
if err := json.Unmarshal(pktJson, &pkt); err != nil {
	return err
}
```
I also added in some more formatting restriction checks for the PKToken based on what we were doing before but more verbose.

### OpSigGQ Removed
We used to have this struct variable called `OpSigGQ` which essentially said whether or not our `Op` signature was GQ or not. I didn't find this to be very extensible and I thought we could accomplish this with a simple function call for now. Here is the new function in action.
```golang
sigType, ok := pkt.ProviderSignatureType()
if !ok {
	return fmt.Errorf("provider signature type missing")
}

if sigType == pktoken.Gq {
    ...
}
```

### Condensations

1. Before, we had a function per signature type to generate the compact form, now:
```golang
func (p *PKToken) Compact(sig *jws.Signature) ([]byte, error)
```
3. Before, we had several functions to add signatures to our PkToken, now:
```golang
func (p *PKToken) AddSignature(token []byte, sigType SignatureType) error
```

### Establishing New Payload Parsing Pattern
I thought it would be healthier to start using an actual pattern for unmarshaling the payload which is stored as its raw, decoded bytes in our `PKToken`. To me, this pattern makes the most sense because it takes care of key-ing the map and parsing the values at the same time with minimal code.
```golang
var payload struct {
	Issuer   string `json:"iss"`
	Audience string `json:"aud"`
	Email    string `json:"sub"`
}
if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
	return nil, err
}
```

### Parsing the CIC
Before, we were parsing our client instance claims (cic) in the pktoken and returning them. I wanted to keep all the knowledge of the construction and special headers in one place, so I've created a new function: `clientinstance.ParseClaims()`, that allows you to hydrate our claims and then interact with the object directly, getting any variable you might need like `UserPublicKey()` or calculating the commitment in exactly the same way and place it was calculated the first time around. 

### Misc

1. I had to rejig some testing to get it all working correctly and reliably


Patch:
diff --git a/cert/ca.go b/cert/ca.go
index 4e1a39d..54a1bae 100644
--- a/cert/ca.go
+++ b/cert/ca.go
@@ -8,6 +8,7 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/asn1"
+	"encoding/json"
 	"encoding/pem"
 	"fmt"
 	"math/big"
@@ -58,12 +59,12 @@ func GenCAKeyPair() ([]byte, *ecdsa.PrivateKey, error) {
 }
 
 func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, requiredAudience string) ([]byte, error) {
-
-	pkt, err := pktoken.FromJSON(pktJson)
-	if err != nil {
+	var pkt *pktoken.PKToken
+	if err := json.Unmarshal(pktJson, &pkt); err != nil {
 		return nil, err
 	}
-	err = pkt.VerifyCicSig()
+
+	err := pkt.VerifyCicSig()
 	if err != nil {
 		return nil, err
 	}
@@ -78,13 +79,17 @@ func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, require
 	// 	return nil, err
 	// }
 
-	iss, aud, email, err := pkt.GetClaims()
-	if err != nil {
+	var payload struct {
+		Issuer   string   `json:"iss"`
+		Audience []string `json:"aud"`
+		Email    string   `json:"email"`
+	}
+	if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
 		return nil, err
 	}
 
-	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))
+	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)
 	}
 
 	caTemplate, err := x509.ParseCertificate(caBytes)
@@ -92,13 +97,10 @@ 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{subject},
+		EmailAddresses: []string{payload.Email},
 		NotBefore:      time.Now().Add(-1 * time.Minute),
 		NotAfter:       time.Now().Add(time.Hour),
 		KeyUsage:       x509.KeyUsageDigitalSignature,
@@ -108,24 +110,19 @@ 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(oidcIssuer),
+			Value:    []byte(payload.Issuer),
 		}},
 		SubjectKeyId: []byte(util.Base64EncodeForJWT(pktJson)),
 	}
 
-	_, _, upkjwk, err := pkt.GetCicValues()
+	cic, err := pkt.GetCicValues()
 	if err != nil {
 		return nil, err
 	}
-
-	upk, err := upkjwk.PublicKey()
-	if err != nil {
-		return nil, err
-	}
-	_ = upk
+	upk := cic.PublicKey()
 
 	var rawkey interface{} // This is the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
-	if err := upkjwk.Raw(&rawkey); err != nil {
+	if err := upk.Raw(&rawkey); err != nil {
 		return nil, err
 	}
 	pk := rawkey.(*ecdsa.PublicKey)
@@ -147,5 +144,4 @@ func PktTox509(pktJson []byte, caBytes []byte, caPkSk *ecdsa.PrivateKey, require
 	}
 
 	return pemSubCert.Bytes(), nil
-
 }
diff --git a/examples/google/example.go b/examples/google/example.go
index 250d8da..d127c24 100644
--- a/examples/google/example.go
+++ b/examples/google/example.go
@@ -47,7 +47,7 @@ func main() {
 	keyAlgorithm := jwa.ES256
 
 	// Directory for saving data
-	outputDir := "opk/google"
+	outputDir := "output/google"
 
 	command := os.Args[1]
 	switch command {
@@ -94,22 +94,15 @@ func login(outputDir string, alg jwa.KeyAlgorithm, signGQ bool) error {
 		return err
 	}
 
-	pktJson, err := pkt.ToJSON()
-	if err != nil {
-		return err
-	}
-
 	// Pretty print our json token
-	var prettyJSON bytes.Buffer
-	if err := json.Indent(&prettyJSON, pktJson, "", "  "); err != nil {
+	pktJson, err := json.MarshalIndent(pkt, "", "  ")
+	if err != nil {
 		return err
 	}
-	fmt.Println(prettyJSON.String())
+	fmt.Println(string(pktJson))
 
 	// Save our signer and pktoken by writing them to a file
-	saveLogin(outputDir, signer.(*ecdsa.PrivateKey), pktJson)
-
-	return nil
+	return saveLogin(outputDir, signer.(*ecdsa.PrivateKey), pkt)
 }
 
 func sign(message string, outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
@@ -129,7 +122,7 @@ func sign(message string, outputDir string, alg jwa.KeyAlgorithm, signGq bool) e
 	fmt.Println("Hash:", hex.EncodeToString(msgHashSum[:]))
 	fmt.Println("Cert:")
 
-	pktJson, err := pkt.ToJSON()
+	pktJson, err := json.Marshal(pkt)
 	if err != nil {
 		return err
 	}
@@ -178,8 +171,8 @@ func googleCert(outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
 		return fmt.Errorf("malformatted skid: %w", err)
 	}
 
-	skidpkt, err := pktoken.FromJSON(skidDecoded)
-	if err != nil {
+	var skidpkt *pktoken.PKToken
+	if err := json.Unmarshal(skidDecoded, skidpkt); err != nil {
 		return fmt.Errorf("failed to extract PK Token from x509 cert: %w", err)
 	}
 
@@ -194,13 +187,21 @@ func googleCert(outputDir string, alg jwa.KeyAlgorithm, signGq bool) error {
 	return nil
 }
 
-func saveLogin(outputDir string, sk *ecdsa.PrivateKey, pktJson []byte) error {
+func saveLogin(outputDir string, sk *ecdsa.PrivateKey, pkt *pktoken.PKToken) error {
+	if err := os.MkdirAll(outputDir, 0777); err != nil {
+		return err
+	}
+
 	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)
 }
 
@@ -217,8 +218,8 @@ func loadLogin(outputDir string) (crypto.Signer, *pktoken.PKToken, error) {
 		return nil, nil, err
 	}
 
-	pkt, err := pktoken.FromJSON(pktJson)
-	if err != nil {
+	var pkt *pktoken.PKToken
+	if err := json.Unmarshal(pktJson, &pkt); err != nil {
 		return nil, nil, err
 	}
 
diff --git a/parties/ca.go b/parties/ca.go
index 3f2da24..2bf18f4 100644
--- a/parties/ca.go
+++ b/parties/ca.go
@@ -8,6 +8,7 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/asn1"
+	"encoding/json"
 	"encoding/pem"
 	"fmt"
 	"math/big"
@@ -168,14 +169,12 @@ func (a *Ca) Serv() {
 }
 
 func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
-
-	pkt, err := pktoken.FromJSON(pktCom)
-
-	if err != nil {
+	var pkt *pktoken.PKToken
+	if err := json.Unmarshal(pktCom, pkt); err != nil {
 		return nil, err
 	}
-	err = pkt.VerifyCicSig()
-	if err != nil {
+
+	if err := pkt.VerifyCicSig(); err != nil {
 		return nil, err
 	}
 
@@ -189,13 +188,17 @@ func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
 	// 	return nil, err
 	// }
 
-	iss, aud, email, err := pkt.GetClaims()
-	if err != nil {
+	var payload struct {
+		Issuer   string `json:"iss"`
+		Audience string `json:"aud"`
+		Email    string `json:"email"`
+	}
+	if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
 		return nil, err
 	}
 
-	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))
+	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)
 	}
 
 	caTemplate, err := x509.ParseCertificate(caBytes)
@@ -203,13 +206,10 @@ 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{subject},
+		EmailAddresses: []string{payload.Email},
 		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(oidcIssuer),
+			Value:    []byte(payload.Issuer),
 		}},
 		SubjectKeyId: []byte(pktCom),
 	}
@@ -231,24 +231,19 @@ func (a *Ca) PktTox509(pktCom []byte, caBytes []byte) ([]byte, error) {
 
 	// subPkSk.PublicKey()
 
-	_, _, upkjwk, err := pkt.GetCicValues()
+	cic, err := pkt.GetCicValues()
 	if err != nil {
 		return nil, err
 	}
 
-	upk, err := upkjwk.PublicKey()
-	if err != nil {
-		return nil, err
-	}
-	_ = upk
+	upk := cic.PublicKey()
 
 	// fmt.Println("In CA Pkt :"+ pkt.)
 
 	var rawkey interface{} // This is the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
-	if err := upkjwk.Raw(&rawkey); err != nil {
+	if err := upk.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/parties/githubclient.go b/parties/githubclient.go
index 9567ac0..b7b9ce3 100644
--- a/parties/githubclient.go
+++ b/parties/githubclient.go
@@ -14,9 +14,7 @@ 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/gq"
 	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/util"
 	"github.com/zitadel/oidc/v2/pkg/client"
 )
 
@@ -149,77 +147,69 @@ func (g *GithubOp) RequestTokens(cicHash string) ([]byte, error) {
 	return []byte(jwt.Value), err
 }
 
-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")
+func (g *GithubOp) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) error {
+	sigType, ok := pkt.ProviderSignatureType()
+	if !ok {
+		return fmt.Errorf("provider signature type missing")
+	}
+
+	if sigType != pktoken.Gq {
+		return fmt.Errorf("github only support gq signatures")
 	}
 
-	cicphJSON, err := util.Base64DecodeForJWT(pkt.CicPH)
+	cic, err := pkt.GetCicValues()
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	expectedAud := string(util.B64SHA3_256(cicphJSON))
+	commitment, err := cic.Hash()
+	if err != nil {
+		return err
+	}
 
-	idt := pkt.OpJWSCompact()
+	idt, err := pkt.Compact(pkt.Op)
+	if err != nil {
+		return err
+	}
 
 	// 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)
+		return 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)")
+	if err := pkt.VerifyGQSig(pubKey.(*rsa.PublicKey), gqSecurityParameter); err != nil {
+		return err
 	}
 
-	_, payloadB64, _, err := jws.SplitCompact(idt)
-	if err != nil {
-		return nil, err
+	var payload struct {
+		Audience string `json:"aud"`
 	}
-
-	payloadJSON, err := util.Base64DecodeForJWT(payloadB64)
-	if err != nil {
-		return nil, fmt.Errorf("failed to decode payload")
+	if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
+		return err
 	}
 
-	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)
+	if payload.Audience != string(commitment) {
+		return fmt.Errorf("nonce doesn't match")
 	}
 
 	err = pkt.VerifyCicSig()
 	if err != nil {
-		return nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+		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 nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+			return 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)
+			return 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
+	return nil
 }
diff --git a/parties/googleclient.go b/parties/googleclient.go
index 5bfb3ed..a9f5cc6 100644
--- a/parties/googleclient.go
+++ b/parties/googleclient.go
@@ -23,9 +23,7 @@ import (
 
 	httphelper "github.com/zitadel/oidc/v2/pkg/http"
 
-	"github.com/openpubkey/openpubkey/gq"
 	"github.com/openpubkey/openpubkey/pktoken"
-	"github.com/openpubkey/openpubkey/util"
 )
 
 var (
@@ -112,45 +110,52 @@ func (g *GoogleOp) RequestTokens(cicHash string) ([]byte, error) {
 	}
 }
 
-func (g *GoogleOp) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (map[string]any, error) {
-	cicphJSON, err := util.Base64DecodeForJWT(pkt.CicPH)
+func (g *GoogleOp) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) error {
+	cic, err := pkt.GetCicValues()
 	if err != nil {
-		return nil, err
+		return err
+	}
+
+	commitment, err := cic.Hash()
+	if err != nil {
+		return err
 	}
 
-	nonce := string(util.B64SHA3_256(cicphJSON))
+	idt, err := pkt.Compact(pkt.Op)
+	if err != nil {
+		return err
+	}
 
-	idt := pkt.OpJWSCompact()
-	if pkt.OpSigGQ {
+	sigType, ok := pkt.ProviderSignatureType()
+	if !ok {
+		return fmt.Errorf("provider signature type missing")
+	}
+
+	if sigType == 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 := g.PublicKey(idt)
 		if err != nil {
-			return nil, fmt.Errorf("failed to get OP public key: %w", err)
+			return 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)")
+
+		if err := pkt.VerifyGQSig(pubKey.(*rsa.PublicKey), gqSecurityParameter); err != nil {
+			return err
 		}
 
-		_, payloadB64, _, err := jws.SplitCompact(idt)
-		if err != nil {
-			return nil, err
+		var payload struct {
+			Nonce string `json:"nonce"`
 		}
-		payloadJSON, err := util.Base64DecodeForJWT(payloadB64)
-		if err != nil {
-			return nil, fmt.Errorf("failed to decode header: %w", err)
+		if err := json.Unmarshal(pkt.Payload, &payload); err != nil {
+			return err
 		}
 
-		var payload map[string]any
-		json.Unmarshal(payloadJSON, &payload)
-		if payload["nonce"] != nonce {
-			return nil, fmt.Errorf("nonce doesn't match")
+		if payload.Nonce != string(commitment) {
+			return 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 })),
+			rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second), rp.WithNonce(func(ctx context.Context) string { return string(commitment) })),
 		}
 
 		googleRP, err := rp.NewRelyingPartyOIDC(
@@ -158,40 +163,34 @@ func (g *GoogleOp) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (
 			options...)
 
 		if err != nil {
-			return nil, fmt.Errorf("failed to create RP to verify token: %w", err)
+			return 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 fmt.Errorf("error verifying OP signature on PK Token (ID Token invalid): %w", err)
 		}
 	}
 
 	err = pkt.VerifyCicSig()
 	if err != nil {
-		return nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+		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 nil, fmt.Errorf("error verifying CIC signature on PK Token: %w", err)
+			return 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)
+			return 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
+	return nil
 }
 
 func (g *GoogleOp) PublicKey(idt []byte) (PublicKey, error) {
diff --git a/parties/mocks.go b/parties/mocks.go
index d70d6c1..ed8f031 100644
--- a/parties/mocks.go
+++ b/parties/mocks.go
@@ -56,8 +56,8 @@ func (m *MockOpenIdProvider) RequestTokens(cicHash string) ([]byte, error) {
 	return signedToken, nil
 }
 
-func (m *MockOpenIdProvider) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) (map[string]any, error) {
-	return nil, nil
+func (m *MockOpenIdProvider) VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) error {
+	return nil
 }
 
 func (m *MockOpenIdProvider) PublicKey(idt []byte) (PublicKey, error) {
diff --git a/parties/opkclient.go b/parties/opkclient.go
index 98beb09..2f5af7e 100644
--- a/parties/opkclient.go
+++ b/parties/opkclient.go
@@ -7,7 +7,6 @@ import (
 
 	"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"
@@ -48,13 +47,13 @@ func (o *OpkClient) OidcAuth(
 	}
 
 	// Define our OIDC nonce as a commitment to the client instance claims
-	nonce, err := cic.Commitment()
+	nonce, err := cic.Hash()
 	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(nonce)
+	idToken, err := o.Op.RequestTokens(string(nonce))
 	if err != nil {
 		return nil, fmt.Errorf("error requesting ID Token: %w", err)
 	}
@@ -83,17 +82,12 @@ func (o *OpkClient) OidcAuth(
 		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.OpSig = gqSig
-		pkt.OpSigGQ = true
+		pkt.AddSignature(gqToken, pktoken.Gq)
 		// TODO: make sure old value of OpSig is fully gone from memory
 	}
 
-	_, err = o.Op.VerifyPKToken(pkt, nil)
+	err = o.Op.VerifyPKToken(pkt, nil)
 	if err != nil {
 		return nil, fmt.Errorf("error verifying PK Token: %w", err)
 	}
@@ -110,7 +104,7 @@ type PublicKey interface {
 // 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)
+	VerifyPKToken(pkt *pktoken.PKToken, cosPk crypto.PublicKey) error
 	PublicKey(idt []byte) (PublicKey, error)
 }
 
diff --git a/pktoken/cic.go b/pktoken/cic.go
new file mode 100644
index 0000000..eebfdcb
--- /dev/null
+++ b/pktoken/cic.go
@@ -0,0 +1,35 @@
+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 9e6a97e..0641ffb 100644
--- a/pktoken/clientinstance/claims.go
+++ b/pktoken/clientinstance/claims.go
@@ -15,6 +15,8 @@ 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
 }
@@ -44,23 +46,56 @@ func NewClaims(publicKey jwk.Key, claims map[string]any) (*Claims, error) {
 	claims["rz"] = rand
 
 	return &Claims{
+		publicKey: publicKey,
 		protected: claims,
 	}, nil
 }
 
-// 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)
+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 "", err
+		return nil, err
+	}
+	upkjwk, err := jwk.ParseKey(upkBytes)
+	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 &Claims{
+		publicKey: upkjwk,
+		protected: protected,
+	}, nil
+}
 
-	digest := util.B64SHA3_256(buf)
+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)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
-	return string(digest), nil
+	return util.B64SHA3_256(buf), nil
 }
 
 // This function signs the payload of the provided token with the protected headers
diff --git a/pktoken/cos.go b/pktoken/cos.go
new file mode 100644
index 0000000..f93bc4f
--- /dev/null
+++ b/pktoken/cos.go
@@ -0,0 +1,98 @@
+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
new file mode 100644
index 0000000..02cd10e
--- /dev/null
+++ b/pktoken/gq.go
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000..c910fb2
--- /dev/null
+++ b/pktoken/mocks/pktoken.go
@@ -0,0 +1,50 @@
+package mocks
+
+import (
+	"crypto"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwk"
+	"github.com/openpubkey/openpubkey/parties"
+	"github.com/openpubkey/openpubkey/pktoken"
+	"github.com/openpubkey/openpubkey/pktoken/clientinstance"
+)
+
+func GenerateMockPKToken(signingKey crypto.Signer, alg jwa.KeyAlgorithm) (*pktoken.PKToken, error) {
+	jwkKey, err := jwk.PublicKeyOf(signingKey)
+	if err != nil {
+		return nil, err
+	}
+	jwkKey.Set(jwk.AlgorithmKey, alg)
+
+	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 := parties.NewMockOpenIdProvider()
+	if err != nil {
+		return nil, err
+	}
+
+	idToken, err := op.RequestTokens(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 2338b2e..1abca59 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) {
-	alg, _, _, err := p.GetCicValues()
+	cic, 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", alg); err != nil {
+	if err := protected.Set("alg", cic.PublicKey().Algorithm()); err != nil {
 		return nil, err
 	}
-	if err := protected.Set("kid", string(pktHash)); err != nil {
+	if err := protected.Set("kid", 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(
-			alg,
+			cic.PublicKey().Algorithm(),
 			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) {
-	alg, _, upk, err := p.GetCicValues()
+	cic, 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() != alg {
-		return nil, fmt.Errorf(`incorrect "alg" header, expected %s but recieved %s`, alg, protected.Algorithm())
+	if protected.Algorithm() != cic.PublicKey().Algorithm() {
+		return nil, fmt.Errorf(`incorrect "alg" header, expected %s but recieved %s`, cic.PublicKey().Algorithm(), 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(alg, upk))
+	_, err = jws.Verify(osm, jws.WithKey(cic.PublicKey().Algorithm(), cic.PublicKey()))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pktoken/pktoken.go b/pktoken/pktoken.go
index 727c522..362270f 100644
--- a/pktoken/pktoken.go
+++ b/pktoken/pktoken.go
@@ -2,13 +2,9 @@ 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"
@@ -16,398 +12,179 @@ import (
 	_ "golang.org/x/crypto/sha3"
 )
 
-const SigTypeHeader = "sig_type"
-const SigTypeOIDC = "oidc"
-const SigTypeGQ = "oidc_gq"
-const SigTypeCIC = "cic"
-const SigTypeCos = "cos"
+const sigTypeHeader = "sig_type"
 
-type JWS struct {
-	Payload    string        `json:"payload"`
-	Signatures []JWSignature `json:"signatures"`
-}
+type SignatureType string
 
-type JWSignature struct {
-	Protected string         `json:"protected"`
-	Public    map[string]any `json:"header"`
-	Signature string         `json:"signature"`
-}
+const (
+	Oidc SignatureType = "oidc"
+	Gq   SignatureType = "oidc_gq"
+	Cic  SignatureType = "cic"
+	Cos  SignatureType = "cos"
+)
 
-type PKToken struct {
-	Payload []byte
-	OpPH    []byte
-	OpSig   []byte
-	OpSigGQ bool
-	CicPH   []byte
-	CicSig  []byte
-	CosPH   []byte
-	CosSig  []byte
+type Signature = jws.Signature
 
+type PKToken struct {
 	raw []byte // the original, raw representation of the object
+
+	Payload []byte     // decoded payload
+	Op      *Signature // Provider Signature
+	Cic     *Signature // Client Signature
+	Cos     *Signature // Cosigner Signature
 }
 
 func New(idToken []byte, cicToken []byte) (*PKToken, error) {
-	opPH, opPayload, opSig, err := jws.SplitCompact(idToken)
-	if err != nil {
+	pkt := &PKToken{}
+	if err := pkt.AddSignature(idToken, Oidc); err != nil {
 		return nil, err
 	}
 
-	cicPH, cicPayload, cicSig, err := jws.SplitCompact(cicToken)
-	if err != nil {
+	if err := pkt.AddSignature(cicToken, Cic); err != nil {
 		return nil, err
 	}
 
-	// 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
+	return pkt, nil
 }
 
-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)
+func (p *PKToken) AddSignature(token []byte, sigType SignatureType) error {
+	message, err := jws.Parse(token)
 	if err != nil {
-		return nil, err
-	}
-
-	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 {
-		panic(fmt.Errorf("Payload can not be nil"))
-	}
-
-	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"))
+		return err
 	}
 
-	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 there is no payload, we set the provided token's payload as current, otherwise
+	// we make sure that the new payload matches current
 	if p.Payload == nil {
-		panic(fmt.Errorf("Payload can not be 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) 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
+		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())
 	}
 
-	var nonce string
-	err = json.Unmarshal(payMap["nonce"], &nonce)
-	if err != nil {
-		return nil, err
-	}
-
-	return []byte(nonce), nil
-}
-
-func (p *PKToken) GetClaims() ([]byte, []byte, []byte, error) {
-	decodePayload, err := base64.RawStdEncoding.DecodeString(string(p.Payload))
-	if err != nil {
-		return nil, nil, nil, err
+	public := jws.NewHeaders()
+	if err := public.Set(sigTypeHeader, string(sigType)); err != nil {
+		return err
 	}
+	signature := message.Signatures()[0].SetPublicHeaders(public)
 
-	var payMap map[string]interface{}
-	err = json.Unmarshal(decodePayload, &payMap)
-	if err != nil {
-		return nil, nil, nil, err
+	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))
 	}
-
-	iss := payMap["iss"].(string)
-	aud := payMap["aud"].(string)
-	email := payMap["email"].(string)
-
-	return []byte(iss), []byte(aud), []byte(email), nil
+	return nil
 }
 
-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
-	}
-
-	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
+func (p *PKToken) ProviderSignatureType() (SignatureType, bool) {
+	sigType, ok := p.Op.PublicHeaders().Get(sigTypeHeader)
+	if !ok {
+		return "", ok
 	}
 
-	return algJwk, rz.(string), upkjwk, nil
+	return SignatureType(sigType.(string)), true
 }
 
-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) Compact(sig *Signature) ([]byte, error) {
+	message := jws.NewMessage().
+		SetPayload(p.Payload).
+		AppendSignature(sig)
+	return jws.Compact(message)
 }
 
-func (p *PKToken) GetCosValues() (*CosPHeader, error) {
-	decodedCosPH, err := base64.RawStdEncoding.DecodeString(string(p.CosPH))
-	if err != nil {
-		return nil, err
+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
+		}
 	}
 
-	var hds *CosPHeader
-	json.Unmarshal(decodedCosPH, &hds)
-
-	return hds, nil
+	hash := util.B64SHA3_256(message)
+	return string(hash), nil
 }
 
-// func (p *PKToken) GetPublicKey() ([]byte error){
-// 	p.GetCicValues()
-// }
+func (p *PKToken) MarshalJSON() ([]byte, error) {
+	message := jws.NewMessage().
+		SetPayload(p.Payload).
+		AppendSignature(p.Op).
+		AppendSignature(p.Cic)
 
-func (p *PKToken) VerifyCicSig() error {
-	cicJwsCom := p.CicJWSCompact()
-
-	alg, _, upk, err := p.GetCicValues()
-	if err != nil {
-		return err
+	if p.Cos != nil {
+		message.AppendSignature(p.Cos)
 	}
 
-	_, err = jws.Verify(cicJwsCom, jws.WithKey(alg, upk))
-	if err != nil {
-		return err
-	}
-
-	// Verified
-	return nil
+	return json.Marshal(message)
 }
 
-func (p *PKToken) AddCosSig(jwsCom []byte) error {
-
-	cosPH, cosPayload, cosSig, err := jws.SplitCompact(jwsCom)
-	if err != nil {
+func (p *PKToken) UnmarshalJSON(data []byte) error {
+	var parsed jws.Message
+	if err := json.Unmarshal(data, &parsed); 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)
-	}
 
-	p.CosPH = cosPH
-	p.CosSig = cosSig
-	return nil
-}
+	p.Payload = parsed.Payload() // base64 decoded
 
-// 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.")
-	}
+	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)
+		}
 
-	cosJwsCom := p.CosJWSCompact()
-	_, err := jws.Verify(cosJwsCom, jws.WithKey(alg, cosPk))
-	if err != nil {
-		return err
-	}
+		sigHeaderString, ok := sigHeader.(string)
+		if !ok {
+			return fmt.Errorf(`provided "%s" is of wrong type, expected string`, sigTypeHeader)
+		}
 
-	hrs, err := p.GetCosValues()
-	if err != nil {
-		return err
+		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)
+		}
 	}
 
-	// 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)
+	// 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)
 	}
 
-	// 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 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)
 	}
 
-	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)
+	if cosCount > 1 {
+		return fmt.Errorf(`only one signature of type "cos" is allowed, found %d`, cosCount)
 	}
 
-	// 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 bf5cc7b..b3c143b 100644
--- a/cert/ca_test.go
+++ b/cert/ca_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jws"
 
 	"github.com/openpubkey/openpubkey/pktoken"
+	"github.com/openpubkey/openpubkey/pktoken/mocks"
 	"github.com/openpubkey/openpubkey/util"
 )
 
@@ -23,38 +24,23 @@ func TestCertCreation(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	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",
-			},
-		},
-	})
+	userAlg := jwa.ES256
+	userSigningKey, err := util.GenKeyPair(userAlg)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	requiredAudience := "184968138938-g1fddl5tglo7mnlbdak8hbsqhhf79f32.apps.googleusercontent.com"
+	mockPkt, err := mocks.GenerateMockPKToken(userSigningKey, userAlg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	pktJson, err := json.Marshal(mockPkt)
+	if err != nil {
+		t.Fatal(err)
+	}
 
+	requiredAudience := "also_me"
 	pemSubCert, err := PktTox509(pktJson, caBytes, caPkSk, requiredAudience)
 	if err != nil {
 		t.Fatal(err)
@@ -69,11 +55,15 @@ func TestCertCreation(t *testing.T) {
 
 	certPubkey := cc.PublicKey.(*ecdsa.PublicKey)
 
-	pkt, err := pktoken.FromJSON(pktJson)
+	var pkt *pktoken.PKToken
+	if err := json.Unmarshal(pktJson, &pkt); err != nil {
+		t.Fatal(err)
+	}
+
+	sigma, err := pkt.Compact(pkt.Cic)
 	if err != nil {
 		t.Fatal(err)
 	}
-	sigma := pkt.CicJWSCompact()
 
 	_, err = jws.Verify(sigma, jws.WithKey(jwa.ES256, certPubkey))
 	if err != nil {
diff --git a/parties/opkclient_test.go b/parties/opkclient_test.go
index 7eb4bb7..5070e9f 100644
--- a/parties/opkclient_test.go
+++ b/parties/opkclient_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/openpubkey/openpubkey/util"
 )
 
-func TestSigner(t *testing.T) {
+func TestClient(t *testing.T) {
 	alg := jwa.ES256
 
 	testCases := []struct {
@@ -43,7 +43,11 @@ func TestSigner(t *testing.T) {
 
 		if tc.gq {
 			// Verify our GQ signature
-			idt := pkt.OpJWSCompact()
+			idt, err := pkt.Compact(pkt.Op)
+			if err != nil {
+				t.Fatal(err)
+			}
+
 			opPubKey, err := op.PublicKey(idt)
 			if err != nil {
 				t.Fatal(err)
diff --git a/pktoken/pktoken_test.go b/pktoken/pktoken_test.go
index d135260..45652ca 100644
--- a/pktoken/pktoken_test.go
+++ b/pktoken/pktoken_test.go
@@ -2,15 +2,16 @@ 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/clientinstance"
+	"github.com/openpubkey/openpubkey/pktoken/mocks"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+
 	"github.com/openpubkey/openpubkey/util"
 )
 
@@ -22,13 +23,7 @@ func TestPkToken(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	jwkKey, err := jwk.PublicKeyOf(signingKey)
-	if err != nil {
-		t.Fatal(err)
-	}
-	jwkKey.Set(jwk.AlgorithmKey, alg)
-
-	pkt, err := generateMockPKToken(signingKey, jwkKey)
+	pkt, err := mocks.GenerateMockPKToken(signingKey, alg)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -58,53 +53,20 @@ func testPkTokenMessageSigning(t *testing.T, pkt *pktoken.PKToken, signingKey cr
 
 func testPkTokenSerialization(t *testing.T, pkt *pktoken.PKToken) {
 	// Test json serialization/deserialization
-	pktJson, err := pkt.ToJSON()
+	pktJson, err := json.Marshal(pkt)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	token, err := pktoken.FromJSON(pktJson)
-	if err != nil {
+	var newPkt *pktoken.PKToken
+	if err := json.Unmarshal(pktJson, &newPkt); err != nil {
 		t.Fatal(err)
 	}
 
-	tokenJson, err := token.ToJSON()
+	newPktJson, err := json.Marshal(newPkt)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	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)
+	require.JSONEq(t, string(pktJson), string(newPktJson))
 }