mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:12:04 +00:00
21aeed4f4e
Phase 0 closure (Path B2, post-rewrite):
addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:
// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).
Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.
Generated via:
addlicense -c "certctl LLC" -y 2026 \
-f cowork/legal/copyright-header.tpl \
-ignore '**/testdata/**' -ignore '**/*_test.go' \
cmd/ internal/
Verification:
find cmd internal -name '*.go' -not -name '*_test.go' \
-not -path '*/testdata/*' \
-exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l
Returns: 0
gofmt clean. Header additions are comments only, no compile impact.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
490 lines
18 KiB
Go
490 lines
18 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
// SignerInfo parser + signature verifier for SCEP PKIMessage.
|
|
//
|
|
// RFC 5652 §5 (SignedData) + RFC 8894 §3.2.1 (SCEP authenticatedAttributes).
|
|
//
|
|
// SCEP RFC 8894 + Intune master bundle Phase 2.2.
|
|
//
|
|
// The wire shape this parses (cited from RFC 5652 §5.3):
|
|
//
|
|
// SignedData ::= SEQUENCE {
|
|
// version INTEGER,
|
|
// digestAlgorithms SET OF AlgorithmIdentifier,
|
|
// encapContentInfo EncapsulatedContentInfo,
|
|
// certificates [0] IMPLICIT SET OF CertificateChoices OPTIONAL,
|
|
// crls [1] IMPLICIT SET OF RevocationInfoChoices OPTIONAL,
|
|
// signerInfos SET OF SignerInfo -- the field this file targets
|
|
// }
|
|
//
|
|
// SignerInfo ::= SEQUENCE {
|
|
// version INTEGER (1|3),
|
|
// sid SignerIdentifier, -- IssuerAndSerial for v1, SubjectKeyId for v3
|
|
// digestAlgorithm AlgorithmIdentifier,
|
|
// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
|
|
// signatureAlgorithm AlgorithmIdentifier,
|
|
// signature OCTET STRING,
|
|
// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL
|
|
// }
|
|
//
|
|
// SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
|
// Attribute ::= SEQUENCE { attrType OID, attrValues SET OF AttributeValue }
|
|
//
|
|
// The CMS signature is computed over the DER re-serialisation of the
|
|
// signedAttrs as a SET OF Attribute (NOT as the [0] IMPLICIT-tagged form
|
|
// it appears as in the wire). RFC 5652 §5.4 spells this out — easy to
|
|
// get wrong, every CMS implementation has hit this.
|
|
|
|
package pkcs7
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"crypto/sha1" //nolint:gosec // SHA-1 is RFC 8894 §3.5.2 baseline; SHA-256 also accepted
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
)
|
|
|
|
// SCEP authenticated-attribute OIDs (RFC 8894 §3.2.1.4).
|
|
var (
|
|
OIDSCEPMessageType = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 2}
|
|
OIDSCEPPKIStatus = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 3}
|
|
OIDSCEPFailInfo = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 4}
|
|
OIDSCEPSenderNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 5}
|
|
OIDSCEPRecipientNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 6}
|
|
OIDSCEPTransactionID = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 7}
|
|
|
|
// CMS standard authenticated-attribute OIDs used by the signature
|
|
// verification (RFC 5652 §11).
|
|
OIDContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
|
|
OIDMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
|
|
OIDSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
|
|
|
|
// CMS digest algorithm OIDs.
|
|
OIDSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
|
|
OIDSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
|
|
OIDSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
|
|
|
|
// Signature algorithm OIDs the verifier accepts.
|
|
OIDRSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}
|
|
OIDRSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
|
|
OIDRSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13}
|
|
OIDECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
|
|
OIDECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
|
|
|
|
// signedData CMS content type (RFC 5652 §5).
|
|
OIDSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
|
|
)
|
|
|
|
// ErrSignerInfoVerify is returned when signature verification fails. Like
|
|
// the EnvelopedData decrypt error, the message text is intentionally
|
|
// generic so the wire response collapses to BadMessageCheck.
|
|
var ErrSignerInfoVerify = errors.New("signerInfo: signature verification failed")
|
|
|
|
// SignerInfo represents an unwrapped CMS signerInfo with its parsed
|
|
// authenticatedAttributes. Used for SCEP POPO verification.
|
|
type SignerInfo struct {
|
|
Version int
|
|
SignerCert *x509.Certificate // device's transient signing cert (from the SignedData certificates field)
|
|
AuthAttributes map[string]asn1.RawValue // keyed by attribute OID dotted-string
|
|
rawSignedAttrs []byte // DER of the [0] IMPLICIT SignedAttributes — used for re-serialisation
|
|
DigestAlgorithm asn1.ObjectIdentifier
|
|
SignatureAlgorithm asn1.ObjectIdentifier
|
|
Signature []byte
|
|
}
|
|
|
|
// SignedData is the parsed top-level SignedData structure with the
|
|
// signers + the optional certificates the SET carries (used to look up
|
|
// the device's transient signing cert by SignerInfo.sid).
|
|
type SignedData struct {
|
|
Version int
|
|
DigestAlgorithms []pkix.AlgorithmIdentifier
|
|
EncapContentType asn1.ObjectIdentifier
|
|
EncapContent []byte // the inner content the SignedData wraps; nil if the wire used external signature
|
|
Certificates []*x509.Certificate
|
|
SignerInfos []*SignerInfo
|
|
}
|
|
|
|
// signedDataASN1 is the ASN.1 unmarshal target for the SignedData
|
|
// structure. Members tagged with their on-the-wire shapes.
|
|
type signedDataASN1 struct {
|
|
Version int
|
|
DigestAlgorithms []pkix.AlgorithmIdentifier `asn1:"set"`
|
|
EncapContentInfo encapContentInfoASN1
|
|
Certificates asn1.RawValue `asn1:"optional,tag:0"` // [0] IMPLICIT SET OF Certificate
|
|
CRLs asn1.RawValue `asn1:"optional,tag:1"`
|
|
SignerInfos []asn1.RawValue `asn1:"set"`
|
|
}
|
|
|
|
type encapContentInfoASN1 struct {
|
|
ContentType asn1.ObjectIdentifier
|
|
Content asn1.RawValue `asn1:"optional,explicit,tag:0"`
|
|
}
|
|
|
|
type signerInfoASN1 struct {
|
|
Version int
|
|
SID asn1.RawValue // CHOICE — IssuerAndSerial (default) or [0] SubjectKeyId
|
|
DigestAlgorithm pkix.AlgorithmIdentifier
|
|
SignedAttrs asn1.RawValue `asn1:"optional,tag:0"` // [0] IMPLICIT SET OF Attribute
|
|
SignatureAlgorithm pkix.AlgorithmIdentifier
|
|
Signature []byte
|
|
UnsignedAttrs asn1.RawValue `asn1:"optional,tag:1"`
|
|
}
|
|
|
|
type attributeASN1 struct {
|
|
Type asn1.ObjectIdentifier
|
|
Values asn1.RawValue `asn1:"set"` // SET OF AttributeValue — left raw; per-attr decoder handles
|
|
}
|
|
|
|
// ParseSignedData parses a CMS ContentInfo wrapping a SignedData and
|
|
// returns the parsed structure including any certs + signerInfos.
|
|
//
|
|
// SCEP clients put the device's transient signing cert in the
|
|
// certificates field; the handler's POPO check picks the cert matching
|
|
// each signerInfo's SID and verifies with that cert's public key.
|
|
func ParseSignedData(der []byte) (*SignedData, error) {
|
|
if len(der) == 0 {
|
|
return nil, fmt.Errorf("signedData: empty input")
|
|
}
|
|
// Try peeling the optional outer ContentInfo (SEQUENCE { OID, [0] EXPLICIT ANY }).
|
|
if peeled, ok := peelContentInfo(der, OIDSignedData); ok {
|
|
der = peeled
|
|
}
|
|
|
|
var raw signedDataASN1
|
|
if _, err := asn1.Unmarshal(der, &raw); err != nil {
|
|
return nil, fmt.Errorf("signedData: parse outer SEQUENCE: %w", err)
|
|
}
|
|
|
|
out := &SignedData{
|
|
Version: raw.Version,
|
|
DigestAlgorithms: raw.DigestAlgorithms,
|
|
EncapContentType: raw.EncapContentInfo.ContentType,
|
|
}
|
|
// EncapContent is [0] EXPLICIT — the [0] EXPLICIT wrapper holds an
|
|
// OCTET STRING whose Bytes are the inner content. Some encoders use
|
|
// a degenerate empty content (external-signature mode); that's fine.
|
|
if len(raw.EncapContentInfo.Content.Bytes) > 0 {
|
|
// The OCTET STRING wrapper inside [0] EXPLICIT — strip it.
|
|
var innerOctet asn1.RawValue
|
|
if _, err := asn1.Unmarshal(raw.EncapContentInfo.Content.Bytes, &innerOctet); err == nil && innerOctet.Tag == asn1.TagOctetString {
|
|
out.EncapContent = innerOctet.Bytes
|
|
} else {
|
|
out.EncapContent = raw.EncapContentInfo.Content.Bytes
|
|
}
|
|
}
|
|
|
|
// Parse certificates SET. Each member is a Certificate (SEQUENCE).
|
|
if len(raw.Certificates.Bytes) > 0 {
|
|
certBytes := raw.Certificates.Bytes
|
|
for len(certBytes) > 0 {
|
|
var rv asn1.RawValue
|
|
rest, err := asn1.Unmarshal(certBytes, &rv)
|
|
if err != nil {
|
|
break
|
|
}
|
|
if rv.Class == asn1.ClassUniversal && rv.Tag == asn1.TagSequence {
|
|
if cert, err := x509.ParseCertificate(rv.FullBytes); err == nil {
|
|
out.Certificates = append(out.Certificates, cert)
|
|
}
|
|
// else: not a parseable cert (could be other CertificateChoices) — skip
|
|
}
|
|
certBytes = rest
|
|
}
|
|
}
|
|
|
|
// Parse each SignerInfo + look up its SignerCert from out.Certificates.
|
|
for _, siRaw := range raw.SignerInfos {
|
|
si, err := parseSignerInfoFromRaw(siRaw, out.Certificates)
|
|
if err != nil {
|
|
// Skip individual unparseable signerInfos rather than failing
|
|
// the whole SignedData — multi-signer CMS may have one bad
|
|
// signer alongside good ones (rare in SCEP, but keep tolerant).
|
|
continue
|
|
}
|
|
out.SignerInfos = append(out.SignerInfos, si)
|
|
}
|
|
// Empty signerInfos is valid for the degenerate certs-only PKCS#7
|
|
// form (RFC 8894 §3.5.1 GetCACert response, RFC 7030 EST cacerts) —
|
|
// a SignedData with only the certificates field populated and no
|
|
// signers. The caller of ParseSignedData decides whether the lack
|
|
// of signers is an error in their context (the SCEP RFC 8894
|
|
// PKIMessage handler treats it as a fall-through to the MVP path;
|
|
// the CertRep certs-only inner content treats it as expected).
|
|
return out, nil
|
|
}
|
|
|
|
// ParseSignerInfos extracts SignerInfo records from a SignedData blob.
|
|
// Convenience wrapper around ParseSignedData when the caller only cares
|
|
// about the signers, not the certificates list.
|
|
func ParseSignerInfos(signedDataDER []byte) ([]*SignerInfo, error) {
|
|
sd, err := ParseSignedData(signedDataDER)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sd.SignerInfos, nil
|
|
}
|
|
|
|
func parseSignerInfoFromRaw(raw asn1.RawValue, certs []*x509.Certificate) (*SignerInfo, error) {
|
|
var siRaw signerInfoASN1
|
|
if _, err := asn1.Unmarshal(raw.FullBytes, &siRaw); err != nil {
|
|
return nil, fmt.Errorf("signerInfo: parse SEQUENCE: %w", err)
|
|
}
|
|
|
|
si := &SignerInfo{
|
|
Version: siRaw.Version,
|
|
AuthAttributes: map[string]asn1.RawValue{},
|
|
DigestAlgorithm: siRaw.DigestAlgorithm.Algorithm,
|
|
SignatureAlgorithm: siRaw.SignatureAlgorithm.Algorithm,
|
|
Signature: siRaw.Signature,
|
|
rawSignedAttrs: siRaw.SignedAttrs.Bytes, // bytes inside the [0] IMPLICIT — used for re-serialisation
|
|
}
|
|
|
|
// Walk authenticated attributes (SET OF Attribute). The [0] IMPLICIT
|
|
// wrapper means siRaw.SignedAttrs.Bytes holds the SET-OF body directly
|
|
// (no extra OCTET STRING wrapper).
|
|
attrBytes := siRaw.SignedAttrs.Bytes
|
|
for len(attrBytes) > 0 {
|
|
var attr attributeASN1
|
|
rest, err := asn1.Unmarshal(attrBytes, &attr)
|
|
if err != nil {
|
|
break
|
|
}
|
|
si.AuthAttributes[attr.Type.String()] = attr.Values
|
|
attrBytes = rest
|
|
}
|
|
|
|
// Resolve SignerCert by matching the SID against the certs list. SCEP
|
|
// uses IssuerAndSerial for v1; the [0] IMPLICIT SubjectKeyId form is
|
|
// v3 — accept both.
|
|
si.SignerCert = matchSignerCert(siRaw.SID, certs)
|
|
if si.SignerCert == nil {
|
|
return nil, fmt.Errorf("signerInfo: SignerCert not found in SignedData certificates")
|
|
}
|
|
return si, nil
|
|
}
|
|
|
|
func matchSignerCert(sid asn1.RawValue, certs []*x509.Certificate) *x509.Certificate {
|
|
// IssuerAndSerial form: SEQUENCE (no context tag) — universal class.
|
|
if sid.Class == asn1.ClassUniversal && sid.Tag == asn1.TagSequence {
|
|
var ias issuerAndSerialASN1
|
|
if _, err := asn1.Unmarshal(sid.FullBytes, &ias); err == nil {
|
|
for _, c := range certs {
|
|
if c.SerialNumber == nil || ias.SerialNumber == nil {
|
|
continue
|
|
}
|
|
if ias.SerialNumber.Cmp(c.SerialNumber) != 0 {
|
|
continue
|
|
}
|
|
if asn1Equal(ias.Issuer.FullBytes, c.RawIssuer) {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// SubjectKeyIdentifier form: [0] IMPLICIT OCTET STRING.
|
|
if sid.Class == asn1.ClassContextSpecific && sid.Tag == 0 {
|
|
ski := sid.Bytes
|
|
for _, c := range certs {
|
|
if asn1Equal(c.SubjectKeyId, ski) {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func asn1Equal(a, b []byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// VerifySignature verifies the signerInfo's signature over the
|
|
// authenticatedAttributes (SCEP POPO).
|
|
//
|
|
// CMS signature semantics (RFC 5652 §5.4):
|
|
//
|
|
// 1. Re-serialise signedAttrs as a SET OF Attribute. The wire form is
|
|
// [0] IMPLICIT, but the signature is computed over the EXPLICIT
|
|
// SET OF re-serialisation. Easy mistake; this is the canonical CMS
|
|
// quirk every implementation hits.
|
|
// 2. Hash the re-serialised bytes with DigestAlgorithm.
|
|
// 3. Verify Signature against the hash using SignerCert.PublicKey +
|
|
// SignatureAlgorithm.
|
|
//
|
|
// Supports RSA-PKCS1v15 + ECDSA. Rejects RSA-PSS as out-of-spec for SCEP.
|
|
func (s *SignerInfo) VerifySignature() error {
|
|
if s == nil || s.SignerCert == nil {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
if len(s.rawSignedAttrs) == 0 {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
|
|
// Re-serialise as SET OF Attribute. We have rawSignedAttrs which is
|
|
// the bytes INSIDE the [0] IMPLICIT wrapper — that's the SET OF body.
|
|
// Wrap with the SET tag (0x31) + length to get the canonical form
|
|
// the signature is computed over.
|
|
signedAttrsForSig := ASN1Wrap(0x31, s.rawSignedAttrs)
|
|
|
|
// Hash with the digest algorithm.
|
|
digest, hashAlg, err := hashForOID(s.DigestAlgorithm, signedAttrsForSig)
|
|
if err != nil {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
|
|
switch pub := s.SignerCert.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
if !isRSASigAlg(s.SignatureAlgorithm) {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
if err := rsa.VerifyPKCS1v15(pub, hashAlg, digest, s.Signature); err != nil {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
return nil
|
|
case *ecdsa.PublicKey:
|
|
if !isECDSASigAlg(s.SignatureAlgorithm) {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
// crypto/ecdsa.VerifyASN1 takes the same hash, returns bool
|
|
if !ecdsa.VerifyASN1(pub, digest, s.Signature) {
|
|
return ErrSignerInfoVerify
|
|
}
|
|
return nil
|
|
default:
|
|
return ErrSignerInfoVerify
|
|
}
|
|
}
|
|
|
|
func hashForOID(oid asn1.ObjectIdentifier, data []byte) ([]byte, crypto.Hash, error) {
|
|
switch {
|
|
case oid.Equal(OIDSHA256), oid.Equal(OIDRSAWithSHA256), oid.Equal(OIDECDSAWithSHA256):
|
|
h := sha256.Sum256(data)
|
|
return h[:], crypto.SHA256, nil
|
|
case oid.Equal(OIDSHA512), oid.Equal(OIDRSAWithSHA512), oid.Equal(OIDECDSAWithSHA512):
|
|
h := sha512.Sum512(data)
|
|
return h[:], crypto.SHA512, nil
|
|
case oid.Equal(OIDSHA1), oid.Equal(OIDRSAWithSHA1):
|
|
// SHA-1 still appears in legacy SCEP clients (Cisco IOS pre-2018).
|
|
// RFC 8894 §3.5.2 advertises SHA-256 as preferred but does not ban SHA-1.
|
|
h := sha1.Sum(data) //nolint:gosec // RFC 8894 §3.5.2 baseline
|
|
return h[:], crypto.SHA1, nil
|
|
}
|
|
return nil, 0, fmt.Errorf("unsupported digest algorithm: %v", oid)
|
|
}
|
|
|
|
func isRSASigAlg(oid asn1.ObjectIdentifier) bool {
|
|
return oid.Equal(OIDRSAWithSHA1) || oid.Equal(OIDRSAWithSHA256) || oid.Equal(OIDRSAWithSHA512) || oid.Equal(OIDRSAEncryption)
|
|
}
|
|
|
|
func isECDSASigAlg(oid asn1.ObjectIdentifier) bool {
|
|
return oid.Equal(OIDECDSAWithSHA256) || oid.Equal(OIDECDSAWithSHA512)
|
|
}
|
|
|
|
// --- SCEP authenticated-attribute extractors -----------------------------
|
|
|
|
// GetMessageType returns the SCEP messageType value (RFC 8894 §3.2.1.4.1
|
|
// — encoded as a PrintableString containing the decimal ASCII of the
|
|
// message type integer, e.g. "19" for PKCSReq).
|
|
func (s *SignerInfo) GetMessageType() (domain.SCEPMessageType, error) {
|
|
str, err := s.attrPrintableString(OIDSCEPMessageType)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
mt, err := strconv.Atoi(str)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("messageType: parse %q as integer: %w", str, err)
|
|
}
|
|
return domain.SCEPMessageType(mt), nil
|
|
}
|
|
|
|
// GetTransactionID returns the SCEP transactionID (RFC 8894 §3.2.1.4.4 —
|
|
// PrintableString chosen by the client; server MUST echo verbatim in
|
|
// CertRep).
|
|
func (s *SignerInfo) GetTransactionID() (string, error) {
|
|
return s.attrPrintableString(OIDSCEPTransactionID)
|
|
}
|
|
|
|
// GetSenderNonce returns the 16-byte SCEP senderNonce (RFC 8894 §3.2.1.4.5
|
|
// — OCTET STRING).
|
|
func (s *SignerInfo) GetSenderNonce() ([]byte, error) {
|
|
return s.attrOctetString(OIDSCEPSenderNonce)
|
|
}
|
|
|
|
// GetMessageDigest returns the standard CMS messageDigest auth-attr
|
|
// (RFC 5652 §11.2). Used by the signature verification — when
|
|
// signedAttrs is present, the signature is over the re-serialised
|
|
// signedAttrs SET; the messageDigest auth-attr is what binds the
|
|
// signedAttrs to the encapContent.
|
|
func (s *SignerInfo) GetMessageDigest() ([]byte, error) {
|
|
return s.attrOctetString(OIDMessageDigest)
|
|
}
|
|
|
|
// attrPrintableString extracts a PrintableString from the AuthAttributes
|
|
// SET-OF-Attribute-Values for the given attribute OID. Caller-side validation
|
|
// of length / charset is left to the SCEP-specific extractor.
|
|
func (s *SignerInfo) attrPrintableString(oid asn1.ObjectIdentifier) (string, error) {
|
|
rv, ok := s.AuthAttributes[oid.String()]
|
|
if !ok {
|
|
return "", fmt.Errorf("auth-attr %v not present", oid)
|
|
}
|
|
// rv is the SET OF AttributeValue — typically one element. The
|
|
// first element is a PrintableString or IA5String.
|
|
if len(rv.Bytes) == 0 {
|
|
return "", fmt.Errorf("auth-attr %v: empty value", oid)
|
|
}
|
|
var inner asn1.RawValue
|
|
if _, err := asn1.Unmarshal(rv.Bytes, &inner); err != nil {
|
|
return "", fmt.Errorf("auth-attr %v: unmarshal value: %w", oid, err)
|
|
}
|
|
// PrintableString / IA5String / UTF8String all carry their bytes
|
|
// directly in inner.Bytes.
|
|
switch inner.Tag {
|
|
case asn1.TagPrintableString, asn1.TagIA5String, asn1.TagUTF8String:
|
|
return string(inner.Bytes), nil
|
|
}
|
|
return "", fmt.Errorf("auth-attr %v: unexpected value tag %d", oid, inner.Tag)
|
|
}
|
|
|
|
func (s *SignerInfo) attrOctetString(oid asn1.ObjectIdentifier) ([]byte, error) {
|
|
rv, ok := s.AuthAttributes[oid.String()]
|
|
if !ok {
|
|
return nil, fmt.Errorf("auth-attr %v not present", oid)
|
|
}
|
|
if len(rv.Bytes) == 0 {
|
|
return nil, fmt.Errorf("auth-attr %v: empty value", oid)
|
|
}
|
|
var inner asn1.RawValue
|
|
if _, err := asn1.Unmarshal(rv.Bytes, &inner); err != nil {
|
|
return nil, fmt.Errorf("auth-attr %v: unmarshal value: %w", oid, err)
|
|
}
|
|
if inner.Tag != asn1.TagOctetString {
|
|
return nil, fmt.Errorf("auth-attr %v: unexpected value tag %d (want OCTET STRING)", oid, inner.Tag)
|
|
}
|
|
return inner.Bytes, nil
|
|
}
|
|
|
|
// silence unused warning for big.Int — referenced via issuerAndSerialASN1 in
|
|
// envelopeddata.go but the linker only sees it once per package; this keeps
|
|
// the import healthy if someone deletes envelopeddata.go's helper struct.
|
|
var _ = (*big.Int)(nil)
|