Skip to content

Commit

Permalink
Merge pull request #43 from TBD54566975/jw-decode
Browse files Browse the repository at this point in the history
`JW*` Decode
  • Loading branch information
KendallWeihe authored Feb 12, 2024
2 parents 4ae198c + 4e7bccb commit 9518922
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 216 deletions.
192 changes: 110 additions & 82 deletions jws/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,39 @@ import (
"github.com/tbd54566975/web5-go/dids/didcore"
)

// 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"`
}
func Decode(jws string) (Decoded, error) {
parts := strings.Split(jws, ".")
if len(parts) != 3 {
return Decoded{}, fmt.Errorf("malformed JWS. Expected 3 parts, got %d", len(parts))
}

type JWSPayload any
header, err := DecodeHeader(parts[0])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode header: %w", err)
}

// Base64UrlEncode returns the base64url encoded header.
func (j Header) Base64UrlEncode() (string, error) {
bytes, err := json.Marshal(j)
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", err
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode payload: %w", err)
}

return base64.RawURLEncoding.EncodeToString(bytes), nil
var payload Payload
err = json.Unmarshal(payloadBytes, &payload)
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to unmarshal payload: %w", err)
}

signature, err := DecodeSignature(parts[2])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode signature: %w", err)
}

return Decoded{
Header: header,
Payload: payload,
Signature: signature,
Parts: parts,
}, nil
}

// DecodeHeader decodes the base64url encoded JWS header.
Expand All @@ -60,31 +63,40 @@ func DecodeHeader(base64UrlEncodedHeader string) (Header, error) {
return header, nil
}

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
}

// SignOpts is a type that represents an option that can be passed to [github.com/tbd54566975/web5-go/jws.Sign].
type SignOpts func(opts *signOpts)
// SignOpt is a type that represents an option that can be passed to [github.com/tbd54566975/web5-go/jws.Sign].
type SignOpt func(opts *signOpts)

// Purpose is an option that can be passed to [github.com/tbd54566975/web5-go/jws.Sign].
// It is used to select the appropriate key to sign with
func Purpose(p string) SignOpts {
func Purpose(p string) SignOpt {
return func(opts *signOpts) {
opts.selector = didcore.Purpose(p)
}
}

func VerificationMethod(id string) SignOpts {
func VerificationMethod(id string) SignOpt {
return func(opts *signOpts) {
opts.selector = didcore.ID(id)
}
}

func VMSelector(selector didcore.VMSelector) SignOpts {
func VMSelector(selector didcore.VMSelector) SignOpt {
return func(opts *signOpts) {
opts.selector = selector
}
Expand All @@ -94,15 +106,15 @@ func VMSelector(selector didcore.VMSelector) SignOpts {
// It is used to indicate whether the payload should be included in the signature.
// More details can be found in [Specification].
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
func DetachedPayload(detached bool) SignOpts {
func DetachedPayload(detached bool) SignOpt {
return func(opts *signOpts) {
opts.detached = detached
}
}

// Purpose is an option that can be passed to [github.com/tbd54566975/web5-go/jws.Sign].
// It is used to select the appropriate key to sign with
func Type(typ string) SignOpts {
func Type(typ string) SignOpt {
return func(opts *signOpts) {
opts.typ = typ
}
Expand All @@ -111,32 +123,32 @@ func Type(typ string) SignOpts {
// 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 JWSPayload, did did.BearerDID, opts ...SignOpts) (string, error) {
func Sign(payload Payload, 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: %s", err.Error())
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: %s", err.Error())
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.Base64UrlEncode()
base64UrlEncodedHeader, err := header.Encode()
if err != nil {
return "", fmt.Errorf("failed to base64 url encode header: %s", err.Error())
return "", fmt.Errorf("failed to base64 url encode header: %w", err)
}

payloadBytes, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal payload: %s", err.Error())
return "", fmt.Errorf("failed to marshal payload: %w", err)
}

base64UrlEncodedPayload := base64.RawURLEncoding.EncodeToString(payloadBytes)
Expand All @@ -146,7 +158,7 @@ func Sign(payload JWSPayload, did did.BearerDID, opts ...SignOpts) (string, erro

signature, err := sign(toSignBytes)
if err != nil {
return "", fmt.Errorf("failed to compute signature: %s", err.Error())
return "", fmt.Errorf("failed to compute signature: %w", err)
}

base64UrlEncodedSignature := base64.RawURLEncoding.EncodeToString(signature)
Expand All @@ -161,72 +173,88 @@ func Sign(payload JWSPayload, did did.BearerDID, opts ...SignOpts) (string, erro
return compactJWS, nil
}

func Verify(compactJWS string) (bool, error) {
parts := strings.Split(compactJWS, ".")
if len(parts) != 3 {
return false, fmt.Errorf("malformed JWS. Expected 3 parts, got %d", len(parts))
}

base64UrlEncodedHeader := parts[0]
header, err := DecodeHeader(base64UrlEncodedHeader)
func Verify(compactJWS string) (Decoded, error) {
decodedJWS, err := Decode(compactJWS)
if err != nil {
return false, fmt.Errorf("malformed JWS. Failed to decode header: %s", err.Error())
return decodedJWS, fmt.Errorf("signature verification failed: %w", err)
}

if header.ALG == "" || header.KID == "" {
return false, fmt.Errorf("malformed JWS header. alg and kid are required")
err = decodedJWS.Verify()

return decodedJWS, err
}

type Decoded struct {
Header Header
Payload Payload
Signature []byte
Parts []string
}

func (jws Decoded) Verify() error {
if jws.Header.ALG == "" || jws.Header.KID == "" {
return fmt.Errorf("malformed JWS header. alg and kid are required")
}

verificationMethodID := header.KID
verificationMethodID := jws.Header.KID
verificationMethodIDParts := strings.Split(verificationMethodID, "#")
if len(verificationMethodIDParts) != 2 {
return false, fmt.Errorf("malformed JWS header. kid must be a DID URL")
return fmt.Errorf("malformed JWS header. kid must be a DID URL")
}

base64UrlEncodedPayload := parts[1]
payloadBytes, err := base64.RawURLEncoding.DecodeString(base64UrlEncodedPayload)
if err != nil {
return false, fmt.Errorf("malformed JWS. Failed to decode payload: %s", err.Error())
}
var didURI = verificationMethodIDParts[0]

var payload JWSPayload
err = json.Unmarshal(payloadBytes, &payload)
resolutionResult, err := dids.Resolve(didURI)
if err != nil {
return false, fmt.Errorf("malformed JWS. Failed to unmarshal payload: %s", err.Error())
return fmt.Errorf("failed to resolve DID: %w", err)
}

base64UrlEncodedSignature := parts[2]
signature, err := base64.RawURLEncoding.DecodeString(base64UrlEncodedSignature)
verificationMethod, err := resolutionResult.Document.SelectVerificationMethod(didcore.ID(verificationMethodID))
if err != nil {
return false, fmt.Errorf("malformed JWS. Failed to decode signature: %s", err.Error())
return fmt.Errorf("kid does not match any verification method %w", err)
}

toVerify := base64UrlEncodedHeader + "." + base64UrlEncodedPayload
toVerifyBytes := []byte(toVerify)

var didURI = verificationMethodIDParts[0]

resolutionResult, err := dids.Resolve(didURI)
toVerify := jws.Parts[0] + "." + jws.Parts[1]
verified, err := dsa.Verify([]byte(toVerify), jws.Signature, *verificationMethod.PublicKeyJwk)
if err != nil {
return false, fmt.Errorf("failed to resolve DID: %w", err)
return fmt.Errorf("failed to verify signature: %w", err)
}

var verificationMethod didcore.VerificationMethod
for _, vm := range resolutionResult.Document.VerificationMethod {
if vm.ID == verificationMethodID {
verificationMethod = vm
break
}
if !verified {
return fmt.Errorf("invalid signature")
}

if verificationMethod == (didcore.VerificationMethod{}) {
return false, fmt.Errorf("no verification method found that matches kid: %s", verificationMethodID)
}
return nil
}

verified, err := dsa.Verify(toVerifyBytes, signature, *verificationMethod.PublicKeyJwk)
// 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 false, fmt.Errorf("failed to verify signature: %s", err.Error())
return "", err
}

return verified, nil
return base64.RawURLEncoding.EncodeToString(bytes), nil
}

type Payload any
Loading

0 comments on commit 9518922

Please sign in to comment.