Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ require (
github.com/go-jose/go-jose/v4 v4.0.4
github.com/golang/snappy v0.0.4
github.com/google/uuid v1.6.0
github.com/gowebpki/jcs v1.0.1
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef
github.com/iancoleman/orderedmap v0.3.0
github.com/kr/pretty v0.3.1
github.com/lib/pq v1.10.9
github.com/miekg/pkcs11 v1.1.1
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,14 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU=
github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
Expand Down Expand Up @@ -238,6 +242,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
Expand Down
169 changes: 169 additions & 0 deletions lib/jsf/jsf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package jsf

import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"fmt"
)

type Signature struct {
Algorithm string `json:"algorithm,omitempty"`
CertificatePath []string `json:"certificatePath,omitempty"`
Value string `json:"value,omitempty"`
}

type SignedJSF struct {
Signature *Signature `json:"signature,omitempty"`
}

type Verifier interface {
Populate([]byte, []byte, *x509.Certificate, crypto.Hash)
Verify() error
}

type BaseVerifier struct {
message []byte
signature []byte
leaf *x509.Certificate
hash crypto.Hash
}

func (v *BaseVerifier) Populate(msg []byte, sig []byte, leaf *x509.Certificate, hash crypto.Hash) {
v.message = msg
v.signature = sig
v.leaf = leaf
v.hash = hash
}

type RSAVerifier struct {
BaseVerifier
}

func (v *RSAVerifier) Verify() error {
rsaKey, ok := v.leaf.PublicKey.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("key is invalid")
}

return rsa.VerifyPKCS1v15(rsaKey, v.hash, v.message, v.signature)
}

type ECDSAVerifier struct {
BaseVerifier
}

func (v *ECDSAVerifier) Verify() error {
ecdsaKey, ok := v.leaf.PublicKey.(*ecdsa.PublicKey)
if !ok {
return fmt.Errorf("key is invalid")
}
ok = ecdsa.VerifyASN1(ecdsaKey, v.message, v.signature)

if !ok {
return fmt.Errorf("verification failed")
}

return nil
}

func CreateVerifier(algorithm string, leaf *x509.Certificate, msg []byte, sig []byte) (Verifier, error) {
var hash crypto.Hash
var verifier Verifier

switch algorithm {
case "RS256":
hash = crypto.SHA256
verifier = &RSAVerifier{}
case "RS384":
hash = crypto.SHA384
verifier = &RSAVerifier{}
case "RS512":
hash = crypto.SHA512
verifier = &RSAVerifier{}
case "ES256":
hash = crypto.SHA256
verifier = &ECDSAVerifier{}
case "ES384":
hash = crypto.SHA384
verifier = &ECDSAVerifier{}
case "ES512":
hash = crypto.SHA512
verifier = &ECDSAVerifier{}
default:
return nil, fmt.Errorf("unrecognized JSF algorithm: %s", algorithm)
}

msgHash := hash.HashFunc().New()

_, err := msgHash.Write(msg)
if err != nil {
return nil, err
}

verifier.Populate(msgHash.Sum(nil), sig, leaf, hash)

return verifier, nil
}

func (s *Signature) ExtractSignature() string {
signature := s.Value
s.Value = ""

return signature
}

func (s *Signature) ParseCertificatePath() (certs []*x509.Certificate, err error) {
var certsBytes []byte

for _, certStr := range s.CertificatePath {
certBytes, err := base64.RawURLEncoding.DecodeString(certStr)
if err != nil {
return nil, err
}
certsBytes = append(certsBytes, certBytes...)
}
certs, err = x509.ParseCertificates(certsBytes)
if err != nil {
return nil, err
}

return certs, nil
}

func CreateSignature(pubKeyAlg x509.PublicKeyAlgorithm, hash crypto.Hash, certs []*x509.Certificate) (*Signature, error) {
var alg string

// JSF signature algorithms https://cyberphone.github.io/doc/security/jsf.html
// Since relic currently parses rsa and ecdsa private keys, RS256, RS348, RS512, ES256, ES384, ES512 are supported for now.

switch pubKeyAlg {
case x509.RSA:
alg = "RS"
case x509.ECDSA:
alg = "ES"
default:
return nil, fmt.Errorf("unsupported JSF algorithm")
}

switch hash {
case crypto.SHA256:
alg += "256"
case crypto.SHA384:
alg += "384"
case crypto.SHA512:
alg += "512"
}

sig := &Signature{
Algorithm: alg,
}

for _, cert := range certs {
sig.CertificatePath = append(sig.CertificatePath, base64.RawURLEncoding.EncodeToString(cert.Raw))
}

return sig, nil
}
1 change: 1 addition & 0 deletions main_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
_ "github.com/sassoftware/relic/v8/signers/deb"
_ "github.com/sassoftware/relic/v8/signers/dmg"
_ "github.com/sassoftware/relic/v8/signers/jar"
_ "github.com/sassoftware/relic/v8/signers/jsf"
_ "github.com/sassoftware/relic/v8/signers/macho"
_ "github.com/sassoftware/relic/v8/signers/msi"
_ "github.com/sassoftware/relic/v8/signers/pecoff"
Expand Down
158 changes: 158 additions & 0 deletions signers/jsf/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package jsf

import (
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/gowebpki/jcs"
"github.com/iancoleman/orderedmap"

"github.com/sassoftware/relic/v8/lib/certloader"
"github.com/sassoftware/relic/v8/lib/jsf"
"github.com/sassoftware/relic/v8/lib/pkcs7"
"github.com/sassoftware/relic/v8/lib/pkcs9"
"github.com/sassoftware/relic/v8/signers"
)

const indent = " "

var JSFSigner = &signers.Signer{
Name: "jsf",
CertTypes: signers.CertTypeX509,
TestPath: testPath,
Sign: sign,
VerifyStream: verify,
}

func init() {
signers.Register(JSFSigner)
}

func testPath(s string) bool {
return strings.HasSuffix(s, ".json")
}

func sign(r io.Reader, cert *certloader.Certificate, opts signers.SignOpts) ([]byte, error) {
jsonData, err := io.ReadAll(r)
if err != nil {
return nil, err
}

om := orderedmap.New()
if err := json.Unmarshal(jsonData, om); err != nil {
return nil, err
}

// create signature without value
sig, err := jsf.CreateSignature(cert.Leaf.PublicKeyAlgorithm, opts.Hash, cert.Certificates)
if err != nil {
return nil, err
}
om.Set("signature", sig)
jsonBytes, err := json.MarshalIndent(om, "", indent)
if err != nil {
return nil, err
}

canonBytes, err := jcs.Transform(jsonBytes)
if err != nil {
return nil, err
}

msgHash := opts.Hash.HashFunc().New()
_, err = msgHash.Write(canonBytes)
if err != nil {
return nil, err
}

sigBytes, err := cert.Signer().Sign(rand.Reader, msgHash.Sum(nil), opts.Hash)
if err != nil {
return nil, err
}

sig.Value = base64.RawURLEncoding.EncodeToString(sigBytes)
om.Set("signature", sig)

outputJson, err := json.MarshalIndent(om, "", indent)
if err != nil {
return nil, err
}

opts.Audit.SetMimeType("application/json")
return outputJson, nil
}

func verify(r io.Reader, opts signers.VerifyOpts) ([]*signers.Signature, error) {
var s *jsf.SignedJSF
var data map[string]interface{}
var certs []*x509.Certificate

jsonData, err := io.ReadAll(r)
if err != nil {
return nil, err
}

if err := json.Unmarshal(jsonData, &s); err != nil {
return nil, err
}

if err := json.Unmarshal(jsonData, &data); err != nil {
return nil, err
}

signature := s.Signature.ExtractSignature()

sigBytes, err := base64.RawURLEncoding.DecodeString(signature)
if err != nil {
return nil, err
}

// add signature without value field to data
data["signature"] = s.Signature

jsonBytes, err := json.MarshalIndent(data, "", indent)
if err != nil {
return nil, err
}

canonBytes, err := jcs.Transform(jsonBytes)
if err != nil {
return nil, err
}

certs = opts.TrustedX509
if len(certs) == 0 {
certs, err = s.Signature.ParseCertificatePath()
if err != nil {
return nil, fmt.Errorf("certificate could not be parsed: %w", err)
}
if len(certs) == 0 {
return nil, fmt.Errorf("no certificate found")
}
}

verifier, err := jsf.CreateVerifier(s.Signature.Algorithm, certs[0], canonBytes, sigBytes)
if err != nil {
return nil, err
}

if err := verifier.Verify(); err != nil {
return nil, err
}

psig := pkcs7.Signature{Intermediates: certs, Certificate: certs[0]}
if psig.Certificate == nil {
return nil, fmt.Errorf("leaf x509 certificate not found")
}

return []*signers.Signature{{
X509Signature: &pkcs9.TimestampedSignature{
Signature: psig,
},
}}, nil
}