Skip to content

Commit

Permalink
Merge pull request #89 from mdwn/mike.wilson/support-crypto-signer
Browse files Browse the repository at this point in the history
Add API for using crypto.Signer with SigningContext
  • Loading branch information
russellhaering authored Mar 8, 2023
2 parents b317f5f + dcbd738 commit 37d216c
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 57 deletions.
123 changes: 100 additions & 23 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package dsig

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
_ "crypto/sha1"
_ "crypto/sha256"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
Expand All @@ -15,11 +17,18 @@ import (
)

type SigningContext struct {
Hash crypto.Hash
Hash crypto.Hash

// This field will be nil and unused if the SigningContext is created with
// NewSigningContext
KeyStore X509KeyStore
IdAttribute string
Prefix string
Canonicalizer Canonicalizer

// KeyStore is mutually exclusive with signer and certs
signer crypto.Signer
certs [][]byte
}

func NewDefaultSigningContext(ks X509KeyStore) *SigningContext {
Expand All @@ -32,13 +41,54 @@ func NewDefaultSigningContext(ks X509KeyStore) *SigningContext {
}
}

// NewSigningContext creates a new signing context with the given signer and certificate chain.
// Note that e.g. rsa.PrivateKey implements the crypto.Signer interface.
// The certificate chain is a slice of ASN.1 DER-encoded X.509 certificates.
// A SigningContext created with this function should not use the KeyStore field.
// It will return error if passed a nil crypto.Signer
func NewSigningContext(signer crypto.Signer, certs [][]byte) (*SigningContext, error) {
if signer == nil {
return nil, errors.New("signer cannot be nil for NewSigningContext")
}
ctx := &SigningContext{
Hash: crypto.SHA256,
IdAttribute: DefaultIdAttr,
Prefix: DefaultPrefix,
Canonicalizer: MakeC14N11Canonicalizer(),

signer: signer,
certs: certs,
}
return ctx, nil
}

func (ctx *SigningContext) getPublicKeyAlgorithm() x509.PublicKeyAlgorithm {
if ctx.KeyStore != nil {
return x509.RSA
} else {
switch ctx.signer.Public().(type) {
case *ecdsa.PublicKey:
return x509.ECDSA
case *rsa.PublicKey:
return x509.RSA
}
}

return x509.UnknownPublicKeyAlgorithm
}

func (ctx *SigningContext) SetSignatureMethod(algorithmID string) error {
hash, ok := signatureMethodsByIdentifier[algorithmID]
info, ok := signatureMethodsByIdentifier[algorithmID]
if !ok {
return fmt.Errorf("Unknown SignatureMethod: %s", algorithmID)
return fmt.Errorf("unknown SignatureMethod: %s", algorithmID)
}

ctx.Hash = hash
algo := ctx.getPublicKeyAlgorithm()
if info.PublicKeyAlgorithm != algo {
return fmt.Errorf("SignatureMethod %s is incompatible with %s key", algorithmID, algo)
}

ctx.Hash = info.Hash

return nil
}
Expand All @@ -58,6 +108,46 @@ func (ctx *SigningContext) digest(el *etree.Element) ([]byte, error) {
return hash.Sum(nil), nil
}

func (ctx *SigningContext) signDigest(digest []byte) ([]byte, error) {
if ctx.KeyStore != nil {
key, _, err := ctx.KeyStore.GetKeyPair()
if err != nil {
return nil, err
}

rawSignature, err := rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest)
if err != nil {
return nil, err
}

return rawSignature, nil
} else {
rawSignature, err := ctx.signer.Sign(rand.Reader, digest, ctx.Hash)
if err != nil {
return nil, err
}

return rawSignature, nil
}
}

func (ctx *SigningContext) getCerts() ([][]byte, error) {
if ctx.KeyStore != nil {
if cs, ok := ctx.KeyStore.(X509ChainStore); ok {
return cs.GetChain()
}

_, cert, err := ctx.KeyStore.GetKeyPair()
if err != nil {
return nil, err
}

return [][]byte{cert}, nil
} else {
return ctx.certs, nil
}
}

func (ctx *SigningContext) constructSignedInfo(el *etree.Element, enveloped bool) (*etree.Element, error) {
digestAlgorithmIdentifier := ctx.GetDigestAlgorithmIdentifier()
if digestAlgorithmIdentifier == "" {
Expand Down Expand Up @@ -97,7 +187,6 @@ func (ctx *SigningContext) constructSignedInfo(el *etree.Element, enveloped bool
reference.CreateAttr(URIAttr, "#"+dataId)
}


// /SignedInfo/Reference/Transforms
transforms := ctx.createNamespacedElement(reference, TransformsTag)
if enveloped {
Expand Down Expand Up @@ -172,20 +261,12 @@ func (ctx *SigningContext) ConstructSignature(el *etree.Element, enveloped bool)
return nil, err
}

key, cert, err := ctx.KeyStore.GetKeyPair()
rawSignature, err := ctx.signDigest(digest)
if err != nil {
return nil, err
}

certs := [][]byte{cert}
if cs, ok := ctx.KeyStore.(X509ChainStore); ok {
certs, err = cs.GetChain()
if err != nil {
return nil, err
}
}

rawSignature, err := rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest)
certs, err := ctx.getCerts()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -222,7 +303,9 @@ func (ctx *SigningContext) SignEnveloped(el *etree.Element) (*etree.Element, err
}

func (ctx *SigningContext) GetSignatureMethodIdentifier() string {
if ident, ok := signatureMethodIdentifiers[ctx.Hash]; ok {
algo := ctx.getPublicKeyAlgorithm()

if ident, ok := signatureMethodIdentifiers[algo][ctx.Hash]; ok {
return ident
}
return ""
Expand All @@ -247,11 +330,5 @@ func (ctx *SigningContext) SignString(content string) ([]byte, error) {
}
digest := hash.Sum(nil)

var signature []byte
if key, _, err := ctx.KeyStore.GetKeyPair(); err != nil {
return nil, fmt.Errorf("unable to fetch key for signing: %v", err)
} else if signature, err = rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest); err != nil {
return nil, fmt.Errorf("error signing: %v", err)
}
return signature, nil
return ctx.signDigest(digest)
}
51 changes: 48 additions & 3 deletions sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dsig

import (
"crypto"
"crypto/tls"
"encoding/base64"
"testing"

Expand All @@ -12,14 +13,24 @@ import (
func TestSign(t *testing.T) {
randomKeyStore := RandomKeyStoreForTest()
ctx := NewDefaultSigningContext(randomKeyStore)
testSignWithContext(t, ctx, RSASHA256SignatureMethod, crypto.SHA256)
}

func TestNewSigningContext(t *testing.T) {
randomKeyStore := RandomKeyStoreForTest().(*MemoryX509KeyStore)
ctx, err := NewSigningContext(randomKeyStore.privateKey, [][]byte{randomKeyStore.cert})
require.NoError(t, err)
testSignWithContext(t, ctx, RSASHA256SignatureMethod, crypto.SHA256)
}

func testSignWithContext(t *testing.T, ctx *SigningContext, sigMethodID string, digestAlgo crypto.Hash) {
authnRequest := &etree.Element{
Space: "samlp",
Tag: "AuthnRequest",
}
id := "_97e34c50-65ec-4132-8b39-02933960a96a"
authnRequest.CreateAttr("ID", id)
hash := crypto.SHA256.New()
hash := digestAlgo.New()
canonicalized, err := ctx.Canonicalizer.Canonicalize(authnRequest)
require.NoError(t, err)

Expand Down Expand Up @@ -49,7 +60,7 @@ func TestSign(t *testing.T) {

signatureMethodAttr := signatureMethodElement.SelectAttr(AlgorithmAttr)
require.NotEmpty(t, signatureMethodAttr)
require.Equal(t, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", signatureMethodAttr.Value)
require.Equal(t, sigMethodID, signatureMethodAttr.Value)

referenceElement := signedInfo.FindElement("//" + ReferenceTag)
require.NotEmpty(t, referenceElement)
Expand All @@ -73,7 +84,7 @@ func TestSign(t *testing.T) {

digestMethodAttr := digestMethodElement.SelectAttr(AlgorithmAttr)
require.NotEmpty(t, digestMethodElement)
require.Equal(t, "http://www.w3.org/2001/04/xmlenc#sha256", digestMethodAttr.Value)
require.Equal(t, digestAlgorithmIdentifiers[digestAlgo], digestMethodAttr.Value)

digestValueElement := referenceElement.FindElement("//" + DigestValueTag)
require.NotEmpty(t, digestValueElement)
Expand Down Expand Up @@ -126,3 +137,37 @@ func TestSignNonDefaultID(t *testing.T) {
refURI := ref.SelectAttrValue("URI", "")
require.Equal(t, refURI, "#"+id)
}

func TestIncompatibleSignatureMethods(t *testing.T) {
// RSA
randomKeyStore := RandomKeyStoreForTest().(*MemoryX509KeyStore)
ctx, err := NewSigningContext(randomKeyStore.privateKey, [][]byte{randomKeyStore.cert})
require.NoError(t, err)

err = ctx.SetSignatureMethod(ECDSASHA512SignatureMethod)
require.Error(t, err)

// ECDSA
testECDSACert, err := tls.X509KeyPair([]byte(ecdsaCert), []byte(ecdsaKey))
require.NoError(t, err)

ctx, err = NewSigningContext(testECDSACert.PrivateKey.(crypto.Signer), testECDSACert.Certificate)
require.NoError(t, err)

err = ctx.SetSignatureMethod(RSASHA1SignatureMethod)
require.Error(t, err)
}

func TestSignWithECDSA(t *testing.T) {
cert, err := tls.X509KeyPair([]byte(ecdsaCert), []byte(ecdsaKey))
require.NoError(t, err)

ctx, err := NewSigningContext(cert.PrivateKey.(crypto.Signer), cert.Certificate)
require.NoError(t, err)

method := ECDSASHA512SignatureMethod
err = ctx.SetSignatureMethod(method)
require.NoError(t, err)

testSignWithContext(t, ctx, method, crypto.SHA512)
}
19 changes: 2 additions & 17 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dsig

import (
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"errors"
Expand Down Expand Up @@ -213,26 +212,12 @@ func (ctx *ValidationContext) verifySignedInfo(sig *types.Signature, canonicaliz
return err
}

signatureAlgorithm, ok := signatureMethodsByIdentifier[signatureMethodId]
algo, ok := x509SignatureAlgorithmByIdentifier[signatureMethodId]
if !ok {
return errors.New("Unknown signature method: " + signatureMethodId)
}

hash := signatureAlgorithm.New()
_, err = hash.Write(canonical)
if err != nil {
return err
}

hashed := hash.Sum(nil)

pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return errors.New("Invalid public key")
}

// Verify that the private key matching the public key from the cert was what was used to sign the 'SignedInfo' and produce the 'SignatureValue'
err = rsa.VerifyPKCS1v15(pubKey, signatureAlgorithm, hashed[:], decodedSignature)
err = cert.CheckSignature(algo, canonical, decodedSignature)
if err != nil {
return err
}
Expand Down
46 changes: 44 additions & 2 deletions validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,32 @@ yy7YHlSiVX13QH2XTu/iQQ==
-----END CERTIFICATE-----
`

const ecdsaResponse = `<samlp:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="id-e65dcbd76bd33f51c51137855d499382ffcbd235" Version="2.0" IssueInstant="2019-06-14T21:16:16.206Z"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://localhost/saml/acs/</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/><ds:Reference URI="#id-e65dcbd76bd33f51c51137855d499382ffcbd235"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>Uh15pBqpaLb8KW9EnUCSsw1D3UN6IE7cM6c69fwy1xQ=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>MEUCIAwuDhyvbhNE7vfS9oqsGwdao/E8EJSK1mQ8gIEIIOQBAiEAud5l0TQru0m291/XzWvdBJ71HN/hOknOnKXqM7OwXrU=</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIB3jCCAYSgAwIBAgITC3mzvAn7vitNgC2KTnea8hlp8jAKBggqhkjOPQQDAjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDYxMzAwNTYwMVoXDTIxMDYxMjAwNTYwMVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEIa9GeZw9TVMAv7Vnn3bz0DdQstQTIHkSnYfKw6QObxRZJoWvDRcvv2zblCki5FuqTbYqUNeDIQEsKwTJRHUCKjUzBRMB0GA1UdDgQWBBRDic4JRcFytcfX1QkFlsOJVUdrTzAfBgNVHSMEGDAWgBRDic4JRcFytcfX1QkFlsOJVUdrTzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQDF2He80OqZJCe8Fjo0BlS5UsRJ3tChy/ZbmkE2DUaFjgIgKpLzRwr21VdekDagOpZj8ENzJ9YC5w+BwffTRwfkyLE=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="beepboopmeow" IssueInstant="2019-06-14T21:16:16.206Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">urn:extrahop:saml:hopcloud:ra:idp</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/><ds:Reference URI="#beepboopmeow"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>FfMcWntKHiIB8bpyFayq1nK5wtcCHMpCUnowv7/0dBQ=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>MEQCIFXVoJmVBLb+zJKDwnIBUA+Mdp0ww0689pvIDPktROS1AiAimmnSUjzMMflVUJvngeyJta33wVMMObIxcEDNesco5A==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIB3jCCAYSgAwIBAgITC3mzvAn7vitNgC2KTnea8hlp8jAKBggqhkjOPQQDAjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDYxMzAwNTYwMVoXDTIxMDYxMjAwNTYwMVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEIa9GeZw9TVMAv7Vnn3bz0DdQstQTIHkSnYfKw6QObxRZJoWvDRcvv2zblCki5FuqTbYqUNeDIQEsKwTJRHUCKjUzBRMB0GA1UdDgQWBBRDic4JRcFytcfX1QkFlsOJVUdrTzAfBgNVHSMEGDAWgBRDic4JRcFytcfX1QkFlsOJVUdrTzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQDF2He80OqZJCe8Fjo0BlS5UsRJ3tChy/ZbmkE2DUaFjgIgKpLzRwr21VdekDagOpZj8ENzJ9YC5w+BwffTRwfkyLE=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">woof</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2019-06-14T21:17:46.206Z"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2019-06-14T21:13:16.206Z" NotOnOrAfter="2019-06-14T21:17:46.206Z"><saml:AudienceRestriction><saml:Audience></saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2019-06-14T21:16:16.206Z" SessionIndex="beepboopmeow"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">reserved</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">reserved</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">boop@example.com</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>`

const ecdsaCert = `
-----BEGIN CERTIFICATE-----
MIIB3jCCAYSgAwIBAgITC3mzvAn7vitNgC2KTnea8hlp8jAKBggqhkjOPQQDAjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDYxMzAwNTYwMVoXDTIxMDYxMjAw
NTYwMVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBZMBMGByqGSM49AgEGCCqGSM49
AwEHA0IABEIa9GeZw9TVMAv7Vnn3bz0DdQstQTIHkSnYfKw6QObxRZJoWvDRcvv2
zblCki5FuqTbYqUNeDIQEsKwTJRHUCKjUzBRMB0GA1UdDgQWBBRDic4JRcFytcfX
1QkFlsOJVUdrTzAfBgNVHSMEGDAWgBRDic4JRcFytcfX1QkFlsOJVUdrTzAPBgNV
HRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQDF2He80OqZJCe8Fjo0BlS5
UsRJ3tChy/ZbmkE2DUaFjgIgKpLzRwr21VdekDagOpZj8ENzJ9YC5w+BwffTRwfk
yLE=
-----END CERTIFICATE-----
`

const ecdsaKey = `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILnLofyDaFeGyDutTFYuWY0u5IVmny1spzfJbCixceI7oAoGCCqGSM49
AwEHoUQDQgAEQhr0Z5nD1NUwC/tWefdvPQN1Cy1BMgeRKdh8rDpA5vFFkmha8NFy
+/bNuUKSLkW6pNtipQ14MhASwrBMlEdQIg==
-----END EC PRIVATE KEY-----
`

func TestDigest(t *testing.T) {
canonicalizer := MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
doc := etree.NewDocument()
Expand Down Expand Up @@ -191,20 +217,36 @@ func TestValidateWithEmptySignatureReference(t *testing.T) {
require.NotEmpty(t, reference)
require.Empty(t, reference.SelectAttr(URIAttr).Value)

block, _ := pem.Decode([]byte(oktaCert))
testValidateDoc(t, doc, oktaCert)
}

func testValidateDoc(t *testing.T, doc *etree.Document, certPEM string) {
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err, "couldn't parse okta cert pem block")
require.NoError(t, err, "couldn't parse cert pem block")

certStore := MemoryX509CertificateStore{
Roots: []*x509.Certificate{cert},
}
vc := NewDefaultValidationContext(&certStore)
vc.Clock = NewFakeClockAt(cert.NotBefore)

el, err := vc.Validate(doc.Root())
require.NoError(t, err)
require.NotEmpty(t, el)
}

func TestValidateECDSA(t *testing.T) {
doc := etree.NewDocument()
err := doc.ReadFromBytes([]byte(ecdsaResponse))
require.NoError(t, err)

sig := doc.FindElement("//" + SignatureTag)
require.NotEmpty(t, sig)

testValidateDoc(t, doc, ecdsaCert)
}

const (
validateCert = `
-----BEGIN CERTIFICATE-----
Expand Down
Loading

0 comments on commit 37d216c

Please sign in to comment.