Skip to content

Commit

Permalink
Merge pull request #381 from smallstep/mariano/challenge-password
Browse files Browse the repository at this point in the history
Support for signing CSRs with challengePassword
  • Loading branch information
maraino authored Dec 6, 2023
2 parents d9eda02 + 7a84055 commit 41cf778
Show file tree
Hide file tree
Showing 6 changed files with 480 additions and 19 deletions.
61 changes: 44 additions & 17 deletions x509util/algorithms.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package x509util

import (
"crypto"
"crypto/x509"
"encoding/asn1"
"strings"

"github.com/pkg/errors"
Expand All @@ -27,27 +29,52 @@ const (
PureEd25519 = "Ed25519"
)

var (
oidSignatureMD2WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 2}
oidSignatureMD5WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4}
oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}
oidSignatureSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
oidSignatureSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12}
oidSignatureSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13}
oidSignatureRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10}
oidSignatureDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3}
oidSignatureDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2}
oidSignatureECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1}
oidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
oidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}
oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
oidSignatureEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}

// oidISOSignatureSHA1WithRSA means the same as oidSignatureSHA1WithRSA but
// it's specified by ISO. Microsoft's makecert.exe has been known to produce
// certificates with this OID.
oidISOSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 29}
)

var signatureAlgorithmMapping = []struct {
name string
value x509.SignatureAlgorithm
oid asn1.ObjectIdentifier
hash crypto.Hash
}{
{"", x509.UnknownSignatureAlgorithm},
{MD2WithRSA, x509.MD2WithRSA},
{MD5WithRSA, x509.MD5WithRSA},
{SHA1WithRSA, x509.SHA1WithRSA},
{SHA256WithRSA, x509.SHA256WithRSA},
{SHA384WithRSA, x509.SHA384WithRSA},
{SHA512WithRSA, x509.SHA512WithRSA},
{DSAWithSHA1, x509.DSAWithSHA1},
{DSAWithSHA256, x509.DSAWithSHA256},
{ECDSAWithSHA1, x509.ECDSAWithSHA1},
{ECDSAWithSHA256, x509.ECDSAWithSHA256},
{ECDSAWithSHA384, x509.ECDSAWithSHA384},
{ECDSAWithSHA512, x509.ECDSAWithSHA512},
{SHA256WithRSAPSS, x509.SHA256WithRSAPSS},
{SHA384WithRSAPSS, x509.SHA384WithRSAPSS},
{SHA512WithRSAPSS, x509.SHA512WithRSAPSS},
{PureEd25519, x509.PureEd25519},
{"", x509.UnknownSignatureAlgorithm, nil, crypto.Hash(0)},
{MD2WithRSA, x509.MD2WithRSA, oidSignatureMD2WithRSA, crypto.Hash(0) /* no value for MD2 */},
{MD5WithRSA, x509.MD5WithRSA, oidSignatureMD5WithRSA, crypto.MD5},
{SHA1WithRSA, x509.SHA1WithRSA, oidSignatureSHA1WithRSA, crypto.SHA1},
{SHA1WithRSA, x509.SHA1WithRSA, oidISOSignatureSHA1WithRSA, crypto.SHA1},
{SHA256WithRSA, x509.SHA256WithRSA, oidSignatureSHA256WithRSA, crypto.SHA256},
{SHA384WithRSA, x509.SHA384WithRSA, oidSignatureSHA384WithRSA, crypto.SHA384},
{SHA512WithRSA, x509.SHA512WithRSA, oidSignatureSHA512WithRSA, crypto.SHA512},
{SHA256WithRSAPSS, x509.SHA256WithRSAPSS, oidSignatureRSAPSS, crypto.SHA256},
{SHA384WithRSAPSS, x509.SHA384WithRSAPSS, oidSignatureRSAPSS, crypto.SHA384},
{SHA512WithRSAPSS, x509.SHA512WithRSAPSS, oidSignatureRSAPSS, crypto.SHA512},
{DSAWithSHA1, x509.DSAWithSHA1, oidSignatureDSAWithSHA1, crypto.SHA1},
{DSAWithSHA256, x509.DSAWithSHA256, oidSignatureDSAWithSHA256, crypto.SHA256},
{ECDSAWithSHA1, x509.ECDSAWithSHA1, oidSignatureECDSAWithSHA1, crypto.SHA1},
{ECDSAWithSHA256, x509.ECDSAWithSHA256, oidSignatureECDSAWithSHA256, crypto.SHA256},
{ECDSAWithSHA384, x509.ECDSAWithSHA384, oidSignatureECDSAWithSHA384, crypto.SHA384},
{ECDSAWithSHA512, x509.ECDSAWithSHA512, oidSignatureECDSAWithSHA512, crypto.SHA512},
{PureEd25519, x509.PureEd25519, oidSignatureEd25519, crypto.Hash(0) /* no pre-hashing */},
}

// SignatureAlgorithm is the JSON representation of the X509 signature algorithms
Expand Down
151 changes: 149 additions & 2 deletions x509util/certificate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,39 @@ import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"

"github.com/pkg/errors"
"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
)

var oidExtensionSubjectAltName = []int{2, 5, 29, 17}
var (
oidExtensionSubjectAltName = []int{2, 5, 29, 17}
oidChallengePassword = []int{1, 2, 840, 113549, 1, 9, 7}
)

type publicKeyInfo struct {
Raw asn1.RawContent
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}

type tbsCertificateRequest struct {
Raw asn1.RawContent
Version int
Subject asn1.RawValue
PublicKey publicKeyInfo
RawAttributes []asn1.RawValue `asn1:"tag:0"`
}

type certificateRequest struct {
Raw asn1.RawContent
TBSCSR tbsCertificateRequest
SignatureAlgorithm pkix.AlgorithmIdentifier
SignatureValue asn1.BitString
}

// CertificateRequest is the JSON representation of an X.509 certificate. It is
// used to build a certificate request from a template.
Expand All @@ -25,6 +52,7 @@ type CertificateRequest struct {
SANs []SubjectAlternativeName `json:"sans"`
Extensions []Extension `json:"extensions"`
SignatureAlgorithm SignatureAlgorithm `json:"signatureAlgorithm"`
ChallengePassword string `json:"-"`
PublicKey interface{} `json:"-"`
PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"-"`
Signature []byte `json:"-"`
Expand Down Expand Up @@ -101,7 +129,7 @@ func NewCertificateRequestFromX509(cr *x509.CertificateRequest) *CertificateRequ
}
}

// GetCertificateRequest returns the equivalent x509.CertificateRequest.
// GetCertificateRequest returns the signed x509.CertificateRequest.
func (c *CertificateRequest) GetCertificateRequest() (*x509.CertificateRequest, error) {
cert := c.GetCertificate().GetCertificate()
asn1Data, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Expand All @@ -116,10 +144,129 @@ func (c *CertificateRequest) GetCertificateRequest() (*x509.CertificateRequest,
if err != nil {
return nil, errors.Wrap(err, "error creating certificate request")
}

// If a challenge password is provided, encode and prepend it as a challenge
// password attribute.
//
// The challengePassword attribute doesn't follow the ASN.1 encoding of
// [pkix.AttributeTypeAndValueSET] used in the deprecated
// [x509.CertificateRequest.Attributes], so this requires some low-level
// ASN.1 operations.
if c.ChallengePassword != "" {
asn1Data, err = c.addChallengePassword(asn1Data)
if err != nil {
return nil, err
}
}

// This should not fail
return x509.ParseCertificateRequest(asn1Data)
}

// addChallengePassword unmarshals the asn1Data into a certificateRequest and
// creates a new one with the challengePassword.
func (c *CertificateRequest) addChallengePassword(asn1Data []byte) ([]byte, error) {
// Build challengePassword attribute (RFC 2985 section-5.4). The resulting
// bytes will be added as an asn1.RawValue in the RawAttributes.
var builder cryptobyte.Builder
builder.AddASN1(cryptobyte_asn1.SEQUENCE, func(child *cryptobyte.Builder) {
child.AddASN1ObjectIdentifier(oidChallengePassword)
child.AddASN1(cryptobyte_asn1.SET, func(value *cryptobyte.Builder) {
switch {
case isPrintableString(c.ChallengePassword, true, true):
value.AddASN1(cryptobyte_asn1.PrintableString, func(s *cryptobyte.Builder) {
s.AddBytes([]byte(c.ChallengePassword))
})
case isUTF8String(c.ChallengePassword):
value.AddASN1(cryptobyte_asn1.UTF8String, func(s *cryptobyte.Builder) {
s.AddBytes([]byte(c.ChallengePassword))
})
default:
value.SetError(errors.New("error marshaling challenge password: password is not valid"))
}
})
})

b, err := builder.Bytes()
if err != nil {
return nil, errors.Wrap(err, "error marshaling challenge password")
}
challengePasswordAttr := asn1.RawValue{
FullBytes: b,
}

// Parse certificate request
var csr certificateRequest

rest, err := asn1.Unmarshal(asn1Data, &csr)
if err != nil {
return nil, err
} else if len(rest) != 0 {
return nil, errors.New("error unmarshalling certificate request: trailing data")
}

sigAlgo := csr.SignatureAlgorithm
tbsCSR := tbsCertificateRequest{
Version: csr.TBSCSR.Version,
Subject: csr.TBSCSR.Subject,
PublicKey: csr.TBSCSR.PublicKey,
RawAttributes: csr.TBSCSR.RawAttributes,
}

// Prepend challengePassword attribute
tbsCSR.RawAttributes = append([]asn1.RawValue{challengePasswordAttr}, tbsCSR.RawAttributes...)

// Marshal tbsCertificateRequest
tbsCSRContents, err := asn1.Marshal(tbsCSR)
if err != nil {
return nil, errors.Wrap(err, "error creating certificate request")
}
tbsCSR.Raw = tbsCSRContents

// Get the hash used previously
var hashFunc crypto.Hash
found := false
sigAlgoOID := sigAlgo.Algorithm
for _, m := range signatureAlgorithmMapping {
if sigAlgoOID.Equal(m.oid) {
hashFunc = m.hash
found = true
break
}
}
if !found {
return nil, errors.Errorf("error creating certificate request: unsupported signature algorithm %s", sigAlgoOID)
}

// Sign tbsCertificateRequest
signed := tbsCSRContents
if hashFunc != 0 {
h := hashFunc.New()
h.Write(signed)
signed = h.Sum(nil)
}

var signature []byte
signature, err = c.Signer.Sign(rand.Reader, signed, hashFunc)
if err != nil {
return nil, errors.Wrap(err, "error creating certificate request")
}

// Build new certificate request and marshal
asn1Data, err = asn1.Marshal(certificateRequest{
TBSCSR: tbsCSR,
SignatureAlgorithm: sigAlgo,
SignatureValue: asn1.BitString{
Bytes: signature,
BitLength: len(signature) * 8,
},
})
if err != nil {
return nil, errors.Wrap(err, "error creating certificate request")
}
return asn1Data, nil
}

// GetCertificate returns the Certificate representation of the
// CertificateRequest.
//
Expand Down
Loading

0 comments on commit 41cf778

Please sign in to comment.