Repository: lestrrat-go/jwx
Pull request #602 (2022-03-04 09:08:41)

Match the APIs for jws/jwe.Compact


Patch:
diff --git a/jwe/message.go b/jwe/message.go
index 9986717b..c9b86f6b 100644
--- a/jwe/message.go
+++ b/jwe/message.go
@@ -431,7 +431,13 @@ func (m *Message) makeDummyRecipient(enckeybuf string, protected Headers) error
 	return nil
 }
 
-func Compact(m *Message) ([]byte, error) {
+// Compact generates a JWE message in compact serialization format from a
+// `*jwe.Message` object. The object contain exactly one recipient, or
+// an error is returned.
+//
+// This function currently does not take any options, but the function
+// signature contains `options` for possible future expansion of the API
+func Compact(m *Message, _ ...CompactOption) ([]byte, error) {
 	if len(m.recipients) != 1 {
 		return nil, fmt.Errorf(`wrong number of recipients for compact serialization`)
 	}
diff --git a/jwe/options.yaml b/jwe/options.yaml
index eb467e5b..c2b65934 100644
--- a/jwe/options.yaml
+++ b/jwe/options.yaml
@@ -1,6 +1,9 @@
 package_name: jwe
 output: jwe/options_gen.go
 interfaces:
+  - name: CompactOption
+    comment: |
+      CompactOption describes options that can be passed to `jwe.Compact`
   - name: DecryptOption
     comment: |
       DecryptOption describes options that can be passed to `jwe.Decrypt`
diff --git a/jwe/options_gen.go b/jwe/options_gen.go
index 79e803d4..e3de0b30 100644
--- a/jwe/options_gen.go
+++ b/jwe/options_gen.go
@@ -9,6 +9,18 @@ import (
 
 type Option = option.Interface
 
+// CompactOption describes options that can be passed to `jwe.Compact`
+type CompactOption interface {
+	Option
+	compactOption()
+}
+
+type compactOption struct {
+	Option
+}
+
+func (*compactOption) compactOption() {}
+
 // DecryptOption describes options that can be passed to `jwe.Decrypt`
 type DecryptOption interface {
 	Option
diff --git a/jwe/serializer.go b/jwe/serializer.go
deleted file mode 100644
index 1ca88c03..00000000
--- a/jwe/serializer.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package jwe
diff --git a/jws/jws.go b/jws/jws.go
index 00e0f4b0..3bbbe23a 100644
--- a/jws/jws.go
+++ b/jws/jws.go
@@ -74,40 +74,6 @@ func (s *payloadSigner) PublicHeader() Headers {
 var signers = make(map[jwa.SignatureAlgorithm]Signer)
 var muSigner = &sync.Mutex{}
 
-// Sign generates a signature for the given payload, and serializes
-// it in compact serialization format. In this format you may NOT use
-// multiple signers.
-//
-// The `alg` parameter is the identifier for the signature algorithm
-// that should be used.
-//
-// For the `key` parameter, any of the following is accepted:
-// * A "raw" key (e.g. rsa.PrivateKey, ecdsa.PrivateKey, etc)
-// * A crypto.Signer
-// * A jwk.Key
-//
-// A `crypto.Signer` is used when the private part of a key is
-// kept in an inaccessible location, such as hardware.
-// `crypto.Signer` is currently supported for RSA, ECDSA, and EdDSA
-// family of algorithms.
-//
-// If the key is a jwk.Key and the key contains a key ID (`kid` field),
-// then it is added to the protected header generated by the signature
-//
-// The algorithm specified in the `alg` parameter must be able to support
-// the type of key you provided, otherwise an error is returned.
-//
-// If you would like to pass custom headers, use the WithHeaders option.
-//
-// If the headers contain "b64" field, then the boolean value for the field
-// is respected when creating the compact serialization form. That is,
-// if you specify a header with `{"b64": false}`, then the payload is
-// not base64 encoded.
-//
-// If you want to use a detached payload, use `jws.WithDetachedPayload()` as
-// one of the options. When you use this option, you must always set the
-// first parameter (`payload`) to `nil`, or the function will return an error
-
 func makeSigner(alg jwa.SignatureAlgorithm, key interface{}, public, protected Headers) (*payloadSigner, error) {
 	muSigner.Lock()
 	signer, ok := signers[alg]
@@ -142,21 +108,54 @@ const (
 var _ = fmtInvalid
 var _ = fmtMax
 
+// Sign generates a signature for the given payload, and serializes
+// it in compact serialization format. In this format you may NOT use
+// multiple signers.
+//
+// The `alg` parameter is the identifier for the signature algorithm
+// that should be used.
+//
+// For the `key` parameter, any of the following is accepted:
+// * A "raw" key (e.g. rsa.PrivateKey, ecdsa.PrivateKey, etc)
+// * A crypto.Signer
+// * A jwk.Key
+//
+// A `crypto.Signer` is used when the private part of a key is
+// kept in an inaccessible location, such as hardware.
+// `crypto.Signer` is currently supported for RSA, ECDSA, and EdDSA
+// family of algorithms.
+//
+// If the key is a jwk.Key and the key contains a key ID (`kid` field),
+// then it is added to the protected header generated by the signature
+//
+// The algorithm specified in the `alg` parameter must be able to support
+// the type of key you provided, otherwise an error is returned.
+//
+// If you would like to pass custom headers, use the WithHeaders option.
+//
+// If the headers contain "b64" field, then the boolean value for the field
+// is respected when creating the compact serialization form. That is,
+// if you specify a header with `{"b64": false}`, then the payload is
+// not base64 encoded.
+//
+// If you want to use a detached payload, use `jws.WithDetachedPayload()` as
+// one of the options. When you use this option, you must always set the
+// first parameter (`payload`) to `nil`, or the function will return an error
 func Sign(payload []byte, options ...SignOption) ([]byte, error) {
 	format := fmtCompact
 	var signers []*payloadSigner
 	var detached bool
-	for _, o := range options {
+	for _, option := range options {
 		//nolint:forcetypeassert
-		switch o.Ident() {
+		switch option.Ident() {
 		case identSerialization{}:
-			format = o.Value().(int)
+			format = option.Value().(int)
 		case identKey{}:
-			data := o.Value().(*withKey)
+			data := option.Value().(*withKey)
 
 			alg, ok := data.alg.(jwa.SignatureAlgorithm)
 			if !ok {
-				return nil, fmt.Errorf(`jws.Sign: expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, o.Value())
+				return nil, fmt.Errorf(`jws.Sign: expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, option.Value())
 			}
 			signer, err := makeSigner(alg, data.key, data.public, data.protected)
 			if err != nil {
@@ -168,7 +167,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
 			if payload != nil {
 				return nil, fmt.Errorf(`jws.Sign: payload must be nil when jws.WithDetachedPayload() is specified`)
 			}
-			payload = o.Value().([]byte)
+			payload = option.Value().([]byte)
 		}
 	}
 
@@ -226,38 +225,11 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
 	case fmtCompact:
 		// Take the only signature object, and convert it into a Compact
 		// serialization format
-		s := result.signatures[0]
-		// XXX check if this is correct
-		hdrs := s.ProtectedHeaders()
-
-		hdrbuf, err := json.Marshal(hdrs)
-		if err != nil {
-			return nil, fmt.Errorf(`jws.Sign: failed to marshal headers: %w`, err)
+		var compactOpts []CompactOption
+		if detached {
+			compactOpts = append(compactOpts, WithDetached(detached))
 		}
-
-		buf := pool.GetBytesBuffer()
-		defer pool.ReleaseBytesBuffer(buf)
-
-		buf.WriteString(base64.EncodeToString(hdrbuf))
-		buf.WriteByte('.')
-
-		if !detached {
-			if getB64Value(hdrs) {
-				encoded := base64.EncodeToString(payload)
-				buf.WriteString(encoded)
-			} else {
-				if bytes.Contains(payload, []byte{'.'}) {
-					return nil, fmt.Errorf(`jws.Sign: payload must not contain a "."`)
-				}
-				buf.Write(payload)
-			}
-		}
-
-		buf.WriteByte('.')
-		buf.WriteString(base64.EncodeToString(s.signature))
-		ret := make([]byte, buf.Len())
-		copy(ret, buf.Bytes())
-		return ret, nil
+		return Compact(&result, compactOpts...)
 	default:
 		return nil, fmt.Errorf(`jws.Sign: invalid serialization format`)
 	}
diff --git a/jws/message.go b/jws/message.go
index c4801ecc..1f7a7d4d 100644
--- a/jws/message.go
+++ b/jws/message.go
@@ -436,3 +436,58 @@ func (m Message) marshalFull() ([]byte, error) {
 	copy(ret, buf.Bytes())
 	return ret, nil
 }
+
+// Compact generates a JWS message in compact serialization format from
+// `*jws.Message` object. The object contain exactly one signature, or
+// an error is returned.
+//
+// If using a detached payload, the payload must already be stored in
+// the `*jws.Message` object, and the `jws.WithDetached()` option
+// must be passed to the function.
+func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
+	if l := len(msg.signatures); l != 1 {
+		return nil, fmt.Errorf(`jws.Compact: cannot serialize message with %d signatures (must be one)`, l)
+	}
+
+	var detached bool
+	for _, option := range options {
+		//nolint:forcetypeassert
+		switch option.Ident() {
+		case identDetached{}:
+			detached = option.Value().(bool)
+		}
+	}
+
+	s := msg.signatures[0]
+	// XXX check if this is correct
+	hdrs := s.ProtectedHeaders()
+
+	hdrbuf, err := json.Marshal(hdrs)
+	if err != nil {
+		return nil, fmt.Errorf(`jws.Compress: failed to marshal headers: %w`, err)
+	}
+
+	buf := pool.GetBytesBuffer()
+	defer pool.ReleaseBytesBuffer(buf)
+
+	buf.WriteString(base64.EncodeToString(hdrbuf))
+	buf.WriteByte('.')
+
+	if !detached {
+		if getB64Value(hdrs) {
+			encoded := base64.EncodeToString(msg.payload)
+			buf.WriteString(encoded)
+		} else {
+			if bytes.Contains(msg.payload, []byte{'.'}) {
+				return nil, fmt.Errorf(`jws.Compress: payload must not contain a "."`)
+			}
+			buf.Write(msg.payload)
+		}
+	}
+
+	buf.WriteByte('.')
+	buf.WriteString(base64.EncodeToString(s.signature))
+	ret := make([]byte, buf.Len())
+	copy(ret, buf.Bytes())
+	return ret, nil
+}
diff --git a/jws/options.yaml b/jws/options.yaml
index 2c2920bb..81946125 100644
--- a/jws/options.yaml
+++ b/jws/options.yaml
@@ -1,6 +1,9 @@
 package_name: jws
 output: jws/options_gen.go
 interfaces:
+  - name: CompactOption
+    comment: |
+      CompactOption describes options that can be passed to `jws.Compact`
   - name: VerifyOption
     comment: |
       VerifyOption describes options that can be passed to `jws.Verify`
@@ -39,6 +42,13 @@ options:
       
       By default `jws.Sign()` will opt to use compact format, so you usually
       do not need to specify this option other than to be explicit about it
+  - ident: Detached
+    interface: CompactOption
+    argument_type: bool
+    comment: |
+      WithDetached specifies that the `jws.Message` should be serialized in
+      JWS compact serialization with detached payload. The resulting octet
+      sequence will not contain the payload section.
   - ident: DetachedPayload
     interface: SignVerifyOption
     argument_type: '[]byte'
diff --git a/jws/options_gen.go b/jws/options_gen.go
index d71a31e8..ae6ad034 100644
--- a/jws/options_gen.go
+++ b/jws/options_gen.go
@@ -10,6 +10,18 @@ import (
 
 type Option = option.Interface
 
+// CompactOption describes options that can be passed to `jws.Compact`
+type CompactOption interface {
+	Option
+	compactOption()
+}
+
+type compactOption struct {
+	Option
+}
+
+func (*compactOption) compactOption() {}
+
 // SignOption describes options that can be passed to `jws.Sign`
 type SignOption interface {
 	Option
@@ -87,6 +99,7 @@ type withKeySuboption struct {
 func (*withKeySuboption) withKeySuboption() {}
 
 type identContext struct{}
+type identDetached struct{}
 type identDetachedPayload struct{}
 type identInferAlgorithmFromKey struct{}
 type identKey struct{}
@@ -104,6 +117,10 @@ func (identContext) String() string {
 	return "WithContext"
 }
 
+func (identDetached) String() string {
+	return "WithDetached"
+}
+
 func (identDetachedPayload) String() string {
 	return "WithDetachedPayload"
 }
@@ -145,7 +162,7 @@ func (identRequireKid) String() string {
 }
 
 func (identSerialization) String() string {
-	return "WithCompact"
+	return "WithSerialization"
 }
 
 func (identUseDefault) String() string {
@@ -156,6 +173,13 @@ func WithContext(v context.Context) VerifyOption {
 	return &verifyOption{option.New(identContext{}, v)}
 }
 
+// WithDetached specifies that the `jws.Message` should be serialized in
+// JWS compact serialization with detached payload. The resulting octet
+// sequence will not contain the payload section.
+func WithDetached(v bool) CompactOption {
+	return &compactOption{option.New(identDetached{}, v)}
+}
+
 // WithDetachedPayload can be used to both sign or verify a JWS message with a
 // detached payload.
 //


Test patch:
diff --git a/jws/options_gen_test.go b/jws/options_gen_test.go
index b2c22b0b..f12179ad 100644
--- a/jws/options_gen_test.go
+++ b/jws/options_gen_test.go
@@ -10,6 +10,7 @@ import (
 
 func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithContext", identContext{}.String())
+	require.Equal(t, "WithDetached", identDetached{}.String())
 	require.Equal(t, "WithDetachedPayload", identDetachedPayload{}.String())
 	require.Equal(t, "WithInferAlgorithmFromKey", identInferAlgorithmFromKey{}.String())
 	require.Equal(t, "WithKey", identKey{}.String())
@@ -20,6 +21,6 @@ func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithProtected", identProtectedHeaders{}.String())
 	require.Equal(t, "WithPublic", identPublicHeaders{}.String())
 	require.Equal(t, "WithRequireKid", identRequireKid{}.String())
-	require.Equal(t, "WithCompact", identSerialization{}.String())
+	require.Equal(t, "WithSerialization", identSerialization{}.String())
 	require.Equal(t, "WithUseDefault", identUseDefault{}.String())
 }