307 lines
9.1 KiB
Go
307 lines
9.1 KiB
Go
package jws
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/beclab/Olares/cli/pkg/web5/crypto/dsa"
|
|
"github.com/beclab/Olares/cli/pkg/web5/dids"
|
|
|
|
_did "github.com/beclab/Olares/cli/pkg/web5/dids/did"
|
|
"github.com/beclab/Olares/cli/pkg/web5/dids/didcore"
|
|
)
|
|
|
|
// Decode decodes the given JWS string into a [Decoded] type
|
|
//
|
|
// # Note
|
|
//
|
|
// The given JWS input is assumed to be a [compact JWS]
|
|
//
|
|
// [compact JWS]: https://datatracker.ietf.org/doc/html/rfc7515#section-7.1
|
|
func Decode(jws string, opts ...DecodeOption) (Decoded, error) {
|
|
o := decodeOptions{}
|
|
|
|
for _, opt := range opts {
|
|
opt(&o)
|
|
}
|
|
|
|
parts := strings.Split(jws, ".")
|
|
if len(parts) != 3 {
|
|
return Decoded{}, fmt.Errorf("malformed JWS. Expected 3 parts, got %d", len(parts))
|
|
}
|
|
|
|
header, err := DecodeHeader(parts[0])
|
|
if err != nil {
|
|
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode header: %w", err)
|
|
}
|
|
|
|
var payload []byte
|
|
if o.payload == nil {
|
|
payload, err = base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode payload: %w", err)
|
|
}
|
|
} else {
|
|
payload = o.payload
|
|
parts[1] = base64.RawURLEncoding.EncodeToString(payload)
|
|
}
|
|
|
|
signature, err := DecodeSignature(parts[2])
|
|
if err != nil {
|
|
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode signature: %w", err)
|
|
}
|
|
|
|
if header.KID == "" {
|
|
return Decoded{}, errors.New("malformed JWS. Expected header to contain kid")
|
|
}
|
|
|
|
signerDID, err := _did.Parse(header.KID)
|
|
if err != nil {
|
|
return Decoded{}, fmt.Errorf("malformed JWS. Failed to parse kid: %w", err)
|
|
}
|
|
|
|
return Decoded{
|
|
Header: header,
|
|
Payload: payload,
|
|
Signature: signature,
|
|
Parts: parts,
|
|
SignerDID: signerDID,
|
|
}, nil
|
|
}
|
|
|
|
type decodeOptions struct {
|
|
payload []byte
|
|
}
|
|
|
|
// DecodeOption represents an option that can be passed to [Decode] or [Verify].
|
|
type DecodeOption func(opts *decodeOptions)
|
|
|
|
// Payload can be passed to [Decode] or [Verify] to provide a detached payload.
|
|
// More info on detached payloads can be found [here].
|
|
//
|
|
// [here]: https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
|
|
func Payload(p []byte) DecodeOption {
|
|
return func(opts *decodeOptions) {
|
|
opts.payload = p
|
|
}
|
|
}
|
|
|
|
// DecodeHeader decodes the base64url encoded JWS header into a [Header]
|
|
func DecodeHeader(base64UrlEncodedHeader string) (Header, error) {
|
|
bytes, err := base64.RawURLEncoding.DecodeString(base64UrlEncodedHeader)
|
|
if err != nil {
|
|
return Header{}, err
|
|
}
|
|
|
|
var header Header
|
|
err = json.Unmarshal(bytes, &header)
|
|
if err != nil {
|
|
return Header{}, err
|
|
}
|
|
|
|
return header, nil
|
|
}
|
|
|
|
// DecodeSignature decodes the base64url encoded JWS signature into a byte array
|
|
func DecodeSignature(base64UrlEncodedSignature string) ([]byte, error) {
|
|
signature, err := base64.RawURLEncoding.DecodeString(base64UrlEncodedSignature)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return signature, nil
|
|
}
|
|
|
|
// options that sign function can take
|
|
type signOpts struct {
|
|
selector didcore.VMSelector
|
|
detached bool
|
|
typ string
|
|
}
|
|
|
|
// SignOpt is a type that represents an option that can be passed to [olares/github.com/beclab/Olares/cli/pkg/web5/jws.Sign].
|
|
type SignOpt func(opts *signOpts)
|
|
|
|
// Purpose is an option that can be passed to [olares/github.com/beclab/Olares/cli/pkg/web5/jws.Sign].
|
|
// It is used to select the appropriate key to sign with
|
|
func Purpose(p string) SignOpt {
|
|
return func(opts *signOpts) {
|
|
opts.selector = didcore.Purpose(p)
|
|
}
|
|
}
|
|
|
|
// VerificationMethod is an option that can be passed to [olares/github.com/beclab/Olares/cli/pkg/web5/jws.Sign].
|
|
// It is used to select the appropriate key to sign with
|
|
func VerificationMethod(id string) SignOpt {
|
|
return func(opts *signOpts) {
|
|
opts.selector = didcore.ID(id)
|
|
}
|
|
}
|
|
|
|
// VMSelector is an option that can be passed to [olares/github.com/beclab/Olares/cli/pkg/web5/jws.Sign].
|
|
// It is used to select the appropriate key to sign with
|
|
func VMSelector(selector didcore.VMSelector) SignOpt {
|
|
return func(opts *signOpts) {
|
|
opts.selector = selector
|
|
}
|
|
}
|
|
|
|
// DetachedPayload is an option that can be passed to [olares/github.com/beclab/Olares/cli/pkg/web5/jws.Sign].
|
|
// It is used to indicate whether the payload should be included in the signature.
|
|
// More details can be found [here].
|
|
//
|
|
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
|
|
func DetachedPayload(detached bool) SignOpt {
|
|
return func(opts *signOpts) {
|
|
opts.detached = detached
|
|
}
|
|
}
|
|
|
|
// Type is an option that can be passed to [olares/github.com/beclab/Olares/cli/pkg/web5/jws.Sign].
|
|
// It is used to set the `typ` JWS header value
|
|
func Type(typ string) SignOpt {
|
|
return func(opts *signOpts) {
|
|
opts.typ = typ
|
|
}
|
|
}
|
|
|
|
// Sign signs the provided payload with a key associated to the provided DID.
|
|
// if no purpose is provided, the default is "assertionMethod". Passing Detached(true)
|
|
// will return a compact JWS with detached content
|
|
func Sign(payload []byte, did _did.BearerDID, opts ...SignOpt) (string, error) {
|
|
o := signOpts{selector: nil, detached: false}
|
|
for _, opt := range opts {
|
|
opt(&o)
|
|
}
|
|
|
|
sign, verificationMethod, err := did.GetSigner(o.selector)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get signer: %w", err)
|
|
}
|
|
|
|
jwa, err := dsa.GetJWA(*verificationMethod.PublicKeyJwk)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to determine alg: %w", err)
|
|
}
|
|
|
|
keyID := did.Document.GetAbsoluteResourceID(verificationMethod.ID)
|
|
header := Header{ALG: jwa, KID: keyID, TYP: o.typ}
|
|
base64UrlEncodedHeader, err := header.Encode()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to base64 url encode header: %w", err)
|
|
}
|
|
|
|
base64UrlEncodedPayload := base64.RawURLEncoding.EncodeToString(payload)
|
|
|
|
toSign := base64UrlEncodedHeader + "." + base64UrlEncodedPayload
|
|
toSignBytes := []byte(toSign)
|
|
|
|
signature, err := sign(toSignBytes)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to compute signature: %w", err)
|
|
}
|
|
|
|
base64UrlEncodedSignature := base64.RawURLEncoding.EncodeToString(signature)
|
|
|
|
var compactJWS string
|
|
if o.detached {
|
|
compactJWS = base64UrlEncodedHeader + "." + "." + base64UrlEncodedSignature
|
|
} else {
|
|
compactJWS = toSign + "." + base64UrlEncodedSignature
|
|
}
|
|
|
|
return compactJWS, nil
|
|
}
|
|
|
|
// Verify verifies the given compactJWS by resolving the DID Document from the kid header value
|
|
// and using the associated public key found by resolving the DID Document
|
|
func Verify(compactJWS string, opts ...DecodeOption) (Decoded, error) {
|
|
decodedJWS, err := Decode(compactJWS, opts...)
|
|
if err != nil {
|
|
return decodedJWS, fmt.Errorf("signature verification failed: %w", err)
|
|
}
|
|
|
|
err = decodedJWS.Verify()
|
|
|
|
return decodedJWS, err
|
|
}
|
|
|
|
// Decoded is a compact JWS decoded into its parts
|
|
type Decoded struct {
|
|
Header Header
|
|
Payload []byte
|
|
Signature []byte
|
|
Parts []string
|
|
SignerDID _did.DID
|
|
}
|
|
|
|
// Verify verifies the given compactJWS by resolving the DID Document from the kid header value
|
|
// and using the associated public key found by resolving the DID Document
|
|
func (jws Decoded) Verify() error {
|
|
if jws.Header.ALG == "" || jws.Header.KID == "" {
|
|
return errors.New("malformed JWS header. alg and kid are required")
|
|
}
|
|
|
|
did, err := _did.Parse(jws.Header.KID)
|
|
if err != nil {
|
|
return errors.New("malformed JWS header. kid must be a DID URL")
|
|
}
|
|
|
|
resolutionResult, err := dids.Resolve(did.URI)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve DID: %w", err)
|
|
}
|
|
|
|
vmSelector := didcore.ID(did.URL())
|
|
verificationMethod, err := resolutionResult.Document.SelectVerificationMethod(vmSelector)
|
|
if err != nil {
|
|
return fmt.Errorf("kid does not match any verification method %w", err)
|
|
}
|
|
|
|
toVerify := jws.Parts[0] + "." + jws.Parts[1]
|
|
|
|
verified, err := dsa.Verify([]byte(toVerify), jws.Signature, *verificationMethod.PublicKeyJwk)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify signature: %w", err)
|
|
}
|
|
|
|
if !verified {
|
|
return errors.New("invalid signature")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Header represents a JWS (JSON Web Signature) header. See [Specification] for more details.
|
|
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#section-4
|
|
type Header struct {
|
|
// Ide ntifies the cryptographic algorithm used to secure the JWS. The JWS Signature value is not
|
|
// valid if the "alg" value does not represent a supported algorithm or if there is not a key for
|
|
// use with that algorithm associated with the party that digitally signed or MACed the content.
|
|
//
|
|
// "alg" values should either be registered in the IANA "JSON Web Signature and Encryption
|
|
// Algorithms" registry or be a value that contains a Collision-Resistant Name. The "alg" value is
|
|
// a case-sensitive ASCII string. This Header Parameter MUST be present and MUST be understood
|
|
// and processed by implementations.
|
|
//
|
|
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1
|
|
ALG string `json:"alg,omitempty"`
|
|
// Key ID Header Parameter https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
|
|
KID string `json:"kid,omitempty"`
|
|
// Type Header Parameter https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9
|
|
TYP string `json:"typ,omitempty"`
|
|
}
|
|
|
|
// Encode returns the base64url encoded header.
|
|
func (j Header) Encode() (string, error) {
|
|
bytes, err := json.Marshal(j)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
|
}
|