Help us shape the future of Go! Take the 2026 Go Developer Survey

scep

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 14 Imported by: 0

README

go-pki-scep

GitHub Release Go Reference License Tests Interop Supply chain OpenSSF Scorecard

go-pki-scep is a Go implementation of the Simple Certificate Enrollment Protocol (SCEP, RFC 8894), the protocol routers, VPN gateways, mobile device managers and other network devices use to request certificates from a CA. It has three packages:

  • scep builds and parses the SCEP messages.
  • scepserver is an HTTP handler that serves SCEP for a CA you implement.
  • scepclient enrolls, renews, polls and queries a SCEP server.

The CMS layer comes from go-pkcs7. Algorithm choices are made per call through option structs. Nothing is configured through package variables.

Contents

Install

go get github.com/misiektoja/go-pki-scep

The module needs Go 1.27.3 or newer. Its only dependency is go-pkcs7. The API may still change before v1.0.0, as CHANGELOG.md records.

Scope

Operation Server handler Client
GetCACaps Lists the capabilities, derived from the options or replaced by them Fetched once and used to pick POST, the digest and the cipher
GetCACert One DER certificate, or the CA and RA certificates in a certificate-only message Sorts the answer into the CA, the encryption recipients and the allowed CertRep signers
GetNextCACert The next CA certificates, signed by the current CA or RA, when the CA implements it Verifies the signature against the current certificates
GetCACertChain The CA certificate and its issuers up to the root in a certificate-only message, when the CA implements it Returns the chain in server order
PKIOperation over GET and POST Both, with a size limit POST when the server lists POSTPKIOperation, GET otherwise
PKCSReq, RenewalReq and UpdateReq Passed to the CA, which issues, keeps the request pending or refuses it Enroll sends PKCSReq and RenewalReq
CertPoll Passed to the CA Poll, and EnrollAndWait, which polls while the answer is PENDING
GetCert and GetCRL Passed to the CA when it implements them GetCert and GetCRL

The scep package handles every RFC 8894 message type in both directions: building and parsing requests, decrypting their content and answering with SUCCESS, PENDING or FAILURE.

GetCACertChain is not part of RFC 8894. Some CAs still serve it for older clients. Its answer carries no RA certificates, so clients enroll with the GetCACert answer.

Getting started

A server implements scepserver.CA and mounts the handler. The CA decides who may enroll, for example by checking the challenge password of a PKCSReq:

handler, err := scepserver.NewHandler(myCA, scepserver.Options{Logger: slog.Default()})
if err != nil {
	return err
}
http.Handle("/scep", handler)

Enroll returns the issued certificate, scepserver.ErrPending to keep a request waiting for approval, a *scepserver.Failure to refuse it or a *scepserver.Busy to ask the client to come back later. The handler refuses some requests without the CA, such as one signed with an ECDSA key or encrypted with a cipher it does not accept. A CA that implements scepserver.RefusalRecorder is told about those as well.

A client fetches the CA certificates, checks them against a fingerprint it trusts and enrolls:

client, err := scepclient.New("https://ca.example.com/scep", scepclient.Options{})
if err != nil {
	return err
}
certs, err := client.GetCACert(ctx)
if err != nil {
	return err
}
// Compare certs.CA with a fingerprint obtained out of band before going further.
res, err := client.EnrollAndWait(ctx, certs, scepclient.EnrollRequest{
	CSR:               csr,
	SignerCertificate: selfSigned,
	SignerKey:         key,
}, 30*time.Second)
if err != nil {
	return err
}
if res.Status != scep.SUCCESS {
	return fmt.Errorf("enrollment refused: %s", res.FailInfo)
}

A PKCSReq is signed with a self-signed certificate for the CSR key. A RenewalReq is signed with the certificate being renewed. x509util.CreateCertificateRequest adds the challenge password to a CSR. For a CSR with an ECDSA key, sign the PKCSReq with a separate RSA key and its self-signed certificate, because the answer is encrypted for the signer certificate.

Security defaults

  • CertRep signers are restricted. The client accepts a CertRep only when one of the CA or RA certificates from GetCACert signed it, even when the message carries its own certificate.
  • Answers are matched to their requests by transaction ID and nonce.
  • Requester authorization belongs to the CA. The handler verifies the signature and decrypts the request, then passes the challenge password and the signer certificate to the CA.
  • CSR signatures are checked before the CA sees the request.
  • Ciphers default to AES-128-CBC. The handler and the client accept AES-CBC and DES-EDE3-CBC. Single DES needs Decrypt in the options of either side. The handler answers with the request cipher unless Options.ContentEncryption sets one.
  • Digests default to SHA-256. MD5 signatures are refused unless AllowMD5 is set.
  • Sizes are bounded: messages on the server, response bodies on the client and the Retry-After delay a server can ask for.
  • Errors inside the CA are answered with HTTP 500 and logged, so the client retries instead of treating the transaction as refused. Internal error text never reaches the client.

Compatibility

The client copes with server behavior that RFC 8894 does not require:

  • A CertRep without a senderNonce or without the signer certificate.
  • A capability list that ends with a NUL byte.
  • An empty 200 OK for an operation the server does not support or for a missing CA name. The client returns ErrEmptyResponse.
  • HTTP 503 without a Retry-After header, sent to throttle requests that arrive close together. The client retries after BusyDelay, five seconds by default, and calls Options.OnBusy before each wait. A busy GetCACaps answer is reported, not taken for a server without capabilities.
  • A PENDING or FAILURE CertRep sent with an HTTP error status, such as 202 or 400. The client reads the CertRep of any answer typed application/x-pki-message and returns HTTPError for other answers.
  • CertRep messages signed with SHA-1.
  • Answers encrypted with single DES whatever the request used, when Options.Decrypt allows it.
  • A GetCACert answer that holds an RA certificate and the root but not the issuing CA, as the OpenXPKI sample configuration sends. CACertificates.CA is then the root, so a certificate issued under an intermediate CA does not verify against it. Enrollment works, because the RA certificate encrypts and signs.

Some differences need a choice by the caller:

  • EJBCA Community Edition 9.6.3 lists Renewal but refuses a RenewalReq. Renew there with a PKCSReq and a new challenge password.
  • A CA with an ECDSA or Ed25519 key needs an RSA RA certificate in its GetCACert answer. Without one, Enroll, Poll, GetCert and GetCRL return ErrNoRSARecipient before they send a request.

The Interop workflow runs these peers on every change:

Peer Direction
sscep v0.10.0 Against the handler: getcaps, getca, enroll with polling, getcert and getcrl
micromdm/scep v2.3.0 client Against the handler: enrollment with a challenge password
micromdm/scep v2.3.0 server The client enrolls, and a wrong challenge password is refused
EJBCA CE 9.6.3 server The client fetches the CA chain and enrolls. A used, wrong or missing password is refused

Documentation

The package documentation on pkg.go.dev describes every exported type and option. Repository files:

Limitations

  • Both ends need RSA keys for encryption. Requests are encrypted for an RSA CA or RA certificate and answers for the request signer certificate, with RSA key transport as RFC 8894 requires. The handler answers a request signed with another key type with badAlg and the cause ErrSignerNotRSA. RFC 8894 section 3.1 encrypts answers for such keys with the challenge password through PasswordRecipientInfo, which go-pki-scep does not support. A CSR may still carry an ECDSA key when an RSA certificate signs the request.
  • No storage. The CA implementation keeps pending requests, issued certificates and CRLs.
  • No CA chain validation in the client. The caller decides whether to trust the GetCACert answer, such as by a pinned fingerprint.
  • No automatic CA rollover. GetNextCACert returns the next certificates and the caller switches to them.
  • No AES-GCM or AuthEnvelopedData.
  • Retry-After is read in seconds. HTTP dates are ignored.

Credits

go-pki-scep continues the work of these projects and their contributors:

  • micromdm/scep, the original implementation by Victor Vrantchan and the MicroMDM contributors.
  • smallstep/scep, the Smallstep fork of the message layer this repository started from.

Both are MIT licensed. NOTICE records the origins of the code, including the Go standard library code in x509util.

Support

SUPPORT.md directs usage questions, bug reports, feature requests and security reports.

License

Licensed under the MIT License. The Go code in x509util keeps the Go BSD-style license in its file header, as NOTICE describes.

Documentation

Overview

Package scep encodes and decodes the pkiMessage structures of the Simple Certificate Enrollment Protocol (SCEP, RFC 8894).

A pkiMessage is a CMS SignedData whose content is an EnvelopedData. The signed attributes carry the message type, the transaction identifier and the nonces. The envelope carries a CSR, a certificate query or the certificates of a response.

Requesters build PKCSReq, RenewalReq, CertPoll, GetCert and GetCRL messages with the New functions. Responders parse them with ParsePKIMessage, decrypt them with DecryptPKIEnvelope and answer with Success, SuccessCRL, Pending or Fail. Algorithm choices are made per call. The HTTP transport lives in the scepserver and scepclient packages.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CACerts

func CACerts(data []byte) ([]*x509.Certificate, error)

CACerts returns the certificates of a certificate-only SignedData, such as a GetCACert or GetNextCACert response body.

func DegenerateCertificates

func DegenerateCertificates(certs []*x509.Certificate) ([]byte, error)

DegenerateCertificates encodes certificates as a certificate-only SignedData, the body of a GetCACert response that carries more than one certificate.

func ParseSignedCertificates

func ParseSignedCertificates(data []byte, signers []*x509.Certificate) ([]*x509.Certificate, error)

ParseSignedCertificates verifies a GetNextCACert response against signers, the current CA or RA certificates from GetCACert, and returns the certificates it announces.

func SignedCertificates

func SignedCertificates(certs []*x509.Certificate, signerCert *x509.Certificate, signerKey crypto.Signer, digest crypto.Hash) ([]byte, error)

SignedCertificates encodes certs as a SignedData that the current CA or RA signs, the body of a GetNextCACert response (RFC 8894 section 4.7). The message carries certs only. The requester already holds the signer certificate from GetCACert. A zero digest selects SHA-256.

Types

type CSRReqMessage

type CSRReqMessage struct {
	// RawDecrypted is the decrypted pkiEnvelope, the DER encoded CSR.
	RawDecrypted []byte

	// CSR is the parsed certificate request.
	CSR *x509.CertificateRequest

	// ChallengePassword is the challengePassword attribute of the CSR, or empty.
	ChallengePassword string
}

CSRReqMessage holds the CSR of a PKCSReq, RenewalReq or UpdateReq.

type CertRepMessage

type CertRepMessage struct {
	PKIStatus
	RecipientNonce
	FailInfo

	// Certificates are the certificates of a SUCCESS response, such as the issued certificate and
	// its chain. Their order is the order the responder sent.
	Certificates []*x509.Certificate

	// CRL is the DER encoded CRL of a SUCCESS response to GetCRL.
	CRL []byte
}

CertRepMessage holds the CertRep attributes and, once the pkiEnvelope of a SUCCESS response is decrypted, its certificates or CRL.

type CertsSelector

type CertsSelector interface {
	SelectCerts([]*x509.Certificate) []*x509.Certificate
}

A CertsSelector filters certificates.

type CertsSelectorFunc

type CertsSelectorFunc func([]*x509.Certificate) []*x509.Certificate

CertsSelectorFunc is a type of function that filters certificates.

func EnciphermentCertsSelector

func EnciphermentCertsSelector() CertsSelectorFunc

EnciphermentCertsSelector returns a CertsSelectorFunc that selects certificates eligible for key encipherment. This certsSelector can be used to filter PKCSReq recipients.

func FingerprintCertsSelector

func FingerprintCertsSelector(hashType crypto.Hash, hash []byte) CertsSelectorFunc

FingerprintCertsSelector selects a certificate that matches hash using hashType against the digest of the raw certificate DER bytes

func NopCertsSelector

func NopCertsSelector() CertsSelectorFunc

NopCertsSelector returns a CertsSelectorFunc that does not do anything.

func (CertsSelectorFunc) SelectCerts

func (f CertsSelectorFunc) SelectCerts(certs []*x509.Certificate) []*x509.Certificate

type FailInfo

type FailInfo string

FailInfo is the SCEP failInfo attribute of a CertRep with status FAILURE.

const (
	BadAlg          FailInfo = "0"
	BadMessageCheck FailInfo = "1"
	BadRequest      FailInfo = "2"
	BadTime         FailInfo = "3"
	BadCertID       FailInfo = "4"
)

Failure reasons from RFC 8894 section 3.2.1.4.

func (FailInfo) String

func (info FailInfo) String() string

String returns the failure reason name and number.

type IssuerAndSerial

type IssuerAndSerial struct {
	// Issuer is the DER encoded name of the issuing CA.
	Issuer []byte
	// SerialNumber is the certificate serial number.
	SerialNumber *big.Int
}

IssuerAndSerial is the content of GetCert and GetCRL: a certificate identified by its issuer name and serial number (RFC 8894 sections 3.3.4 and 3.3.5).

type IssuerAndSubject

type IssuerAndSubject struct {
	// Issuer is the DER encoded name of the CA.
	Issuer []byte
	// Subject is the DER encoded subject of the CSR.
	Subject []byte
}

IssuerAndSubject is the content of a CertPoll: the name of the CA and the subject of the CSR whose certificate the requester waits for (RFC 8894 section 3.3.3).

type MessageType

type MessageType string

MessageType is the SCEP messageType attribute, a decimal number in a PrintableString.

const (
	CertRep    MessageType = "3"
	RenewalReq MessageType = "17"
	UpdateReq  MessageType = "18"
	PKCSReq    MessageType = "19"
	CertPoll   MessageType = "20"
	GetCert    MessageType = "21"
	GetCRL     MessageType = "22"
)

Message types from RFC 8894 section 3.2.1.2. UpdateReq comes from earlier drafts and is parsed like RenewalReq.

func (MessageType) String

func (msg MessageType) String() string

String returns the message type name and number.

type PKIMessage

type PKIMessage struct {
	TransactionID TransactionID
	MessageType   MessageType

	// SenderNonce is required in requests. Some CA products leave it out of CertRep messages.
	SenderNonce SenderNonce

	// CertRepMessage is set on CertRep messages.
	*CertRepMessage

	// CSRReqMessage is set on PKCSReq, RenewalReq and UpdateReq messages once their pkiEnvelope
	// is decrypted, and on built requests.
	*CSRReqMessage

	// PollRequest is set on CertPoll messages once their pkiEnvelope is decrypted.
	PollRequest *IssuerAndSubject

	// CertificateID is set on GetCert and GetCRL messages once their pkiEnvelope is decrypted.
	CertificateID *IssuerAndSerial

	// Raw is the DER encoded pkiMessage.
	Raw []byte

	// SignerCertificate is the certificate that signed the message. A responder encrypts its
	// answer for it.
	SignerCertificate *x509.Certificate

	// Certificates are the certificates carried in the SignedData, which usually include the
	// signer certificate.
	Certificates []*x509.Certificate

	// Digest is the digest algorithm of the signature.
	Digest crypto.Hash

	// ContentEncryption is the content cipher of the pkiEnvelope once it is decrypted. A responder
	// answers with the same cipher.
	ContentEncryption pkcs7.ContentEncryptionAlgorithm

	// Recipients are the CA or RA certificates a built request is encrypted for.
	Recipients []*x509.Certificate
	// contains filtered or unexported fields
}

PKIMessage is a parsed or built SCEP pkiMessage.

func NewCSRRequest

func NewCSRRequest(messageType MessageType, csr *x509.CertificateRequest, opts RequestOptions) (*PKIMessage, error)

NewCSRRequest builds a PKCSReq, RenewalReq or UpdateReq message for csr.

func NewCertPollRequest

func NewCertPollRequest(issuer, subject []byte, opts RequestOptions) (*PKIMessage, error)

NewCertPollRequest builds a CertPoll for the enrollment opts.TransactionID names. issuer is the DER encoded subject of the CA, subject the DER encoded subject of the CSR.

func NewGetCRLRequest

func NewGetCRLRequest(issuer []byte, serial *big.Int, opts RequestOptions) (*PKIMessage, error)

NewGetCRLRequest builds a GetCRL for the CRL that covers the certificate issuer issued with serial.

func NewGetCertRequest

func NewGetCertRequest(issuer []byte, serial *big.Int, opts RequestOptions) (*PKIMessage, error)

NewGetCertRequest builds a GetCert for the certificate that issuer issued with serial.

func ParsePKIMessage

func ParsePKIMessage(data []byte, opts ParseOptions) (*PKIMessage, error)

ParsePKIMessage parses a DER or BER encoded pkiMessage, verifies its signature and reads its attributes. The signature proves possession of the signer key only. Whether the signer is authorized is up to the caller, through ParseOptions.Signers for a response or the challenge password or existing certificate of a request.

func (*PKIMessage) DecryptPKIEnvelope

func (msg *PKIMessage) DecryptPKIEnvelope(cert *x509.Certificate, key crypto.Decrypter, opts pkcs7.DecryptOptions) error

DecryptPKIEnvelope decrypts the pkiEnvelope with the recipient certificate and its RSA key and parses its content: the CSR of an enrollment request, the query of CertPoll, GetCert and GetCRL, or the certificates or CRL of a SUCCESS CertRep. opts lists the content ciphers to accept.

func (*PKIMessage) EnvelopeRecipients

func (msg *PKIMessage) EnvelopeRecipients() ([]IssuerAndSerial, error)

EnvelopeRecipients lists the certificates the pkiEnvelope of a parsed message is encrypted for, so a server that holds several CA or RA keys can pick the one that decrypts it.

func (*PKIMessage) Fail

func (msg *PKIMessage) Fail(info FailInfo, opts ResponseOptions) (*PKIMessage, error)

Fail answers any request with a FAILURE and the reason info.

func (*PKIMessage) MatchesRequest

func (msg *PKIMessage) MatchesRequest(req *PKIMessage) error

MatchesRequest checks that a CertRep answers req: the transaction identifiers are equal and the recipientNonce repeats the senderNonce of req.

func (*PKIMessage) Pending

func (msg *PKIMessage) Pending(opts ResponseOptions) (*PKIMessage, error)

Pending answers an enrollment or CertPoll whose certificate is not issued yet, for example while an operator approves it. The requester then polls with CertPoll.

func (*PKIMessage) Success

func (msg *PKIMessage) Success(certs []*x509.Certificate, opts ResponseOptions) (*PKIMessage, error)

Success answers an enrollment, CertPoll or GetCert with certificates. certs holds the issued or requested certificate first and may add its chain. The certificates are encrypted for the request signer certificate.

func (*PKIMessage) SuccessCRL

func (msg *PKIMessage) SuccessCRL(crl []byte, opts ResponseOptions) (*PKIMessage, error)

SuccessCRL answers a GetCRL with a DER encoded CRL, encrypted for the request signer certificate.

type PKIStatus

type PKIStatus string

PKIStatus is the SCEP pkiStatus attribute of a CertRep.

const (
	SUCCESS PKIStatus = "0"
	FAILURE PKIStatus = "2"
	PENDING PKIStatus = "3"
)

Statuses from RFC 8894 section 3.2.1.3.

func (PKIStatus) String

func (status PKIStatus) String() string

String returns the status name and number.

type ParseOptions

type ParseOptions struct {
	// Signers restricts who may sign the message to these certificates, which are then the only
	// certificates the signature is checked against. A requester sets the CA and RA certificates
	// from GetCACert, so a CertRep that omits its signer certificate still verifies and a CertRep
	// signed by any other key is refused. A responder leaves it empty, because requests are signed
	// with the requester's own certificate.
	Signers []*x509.Certificate

	// AllowMD5 accepts messages signed with the MD5 digest. Some network devices still send them.
	AllowMD5 bool

	// LegacyCertificateParser accepts certificates that mark the authority key identifier
	// critical, which some Windows SCEP clients send.
	LegacyCertificateParser bool
}

ParseOptions controls how ParsePKIMessage reads a message.

type RecipientNonce

type RecipientNonce []byte

RecipientNonce is the recipientNonce of a CertRep. It repeats the senderNonce of the request.

type RequestOptions

type RequestOptions struct {
	// Recipients are the CA or RA certificates the pkiEnvelope is encrypted for, usually the
	// encryption certificate from GetCACert.
	Recipients []*x509.Certificate

	// SignerCertificate and SignerKey sign the request. A PKCSReq is signed with a self-signed
	// certificate for the CSR key, a RenewalReq with the certificate being renewed. CertPoll,
	// GetCert and GetCRL use the same certificate as the enrollment they follow.
	SignerCertificate *x509.Certificate
	SignerKey         crypto.Signer

	// TransactionID identifies the transaction. CertPoll needs the identifier of the enrollment
	// it polls for. When empty, NewCSRRequest derives one from the CSR key and the other
	// functions pick a random one.
	TransactionID TransactionID

	// Digest is the signature digest. The zero value selects SHA-256.
	Digest crypto.Hash

	// Encryption selects the content cipher and the key transport. The zero value selects
	// AES-128-CBC with PKCS #1 v1.5 key transport.
	Encryption pkcs7.EncryptOptions
}

RequestOptions holds what every request needs: who signs it, who it is encrypted for and the algorithms.

type ResponseOptions

type ResponseOptions struct {
	// SignerCertificate and SignerKey sign the response. They belong to the CA or to its RA, the
	// certificate a requester trusts from GetCACert.
	SignerCertificate *x509.Certificate
	SignerKey         crypto.Signer

	// Certificates are carried next to the signer certificate, such as the CA certificate when an
	// RA signs.
	Certificates []*x509.Certificate

	// Digest is the signature digest. The zero value follows the request when it used SHA-1,
	// SHA-256, SHA-384 or SHA-512, or MD5 when the request was signed with MD5, and selects SHA-256
	// otherwise, so a requester gets a digest it can check.
	Digest crypto.Hash

	// ContentEncryption is the cipher of the response pkiEnvelope. The zero value follows the
	// request, or AES-128-CBC when the request envelope was not decrypted.
	ContentEncryption pkcs7.ContentEncryptionAlgorithm
}

ResponseOptions holds what a CertRep needs: who signs it and the algorithms.

type SenderNonce

type SenderNonce []byte

SenderNonce is the random senderNonce of a message, 16 bytes in messages built by this package.

type TransactionID

type TransactionID string

TransactionID identifies an enrollment across its PKCSReq or RenewalReq, its CertPoll messages and their responses. It is a PrintableString.

func CSRTransactionID

func CSRTransactionID(csr *x509.CertificateRequest) (TransactionID, error)

CSRTransactionID derives the transactionID that NewCSRRequest uses when none is set: the hex encoded key identifier of the CSR public key. A requester that polls later can derive it again.

Directories

Path Synopsis
Package cryptoutil derives key identifiers for SCEP transaction identifiers.
Package cryptoutil derives key identifiers for SCEP transaction identifiers.
internal
testca
Package testca is an in-memory SCEP CA for the tests of the server and client packages.
Package testca is an in-memory SCEP CA for the tests of the server and client packages.
Package scepclient is a SCEP (RFC 8894) client over HTTP.
Package scepclient is a SCEP (RFC 8894) client over HTTP.
Package scepserver serves SCEP (RFC 8894) over HTTP for a CA that the caller implements.
Package scepserver serves SCEP (RFC 8894) over HTTP for a CA that the caller implements.
Package x509util creates certificate requests that carry a challengePassword attribute and reads that attribute back.
Package x509util creates certificate requests that carry a challengePassword attribute and reads that attribute back.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL