mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:51:29 +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
140 lines
4.3 KiB
Go
140 lines
4.3 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
// Package pkcs7 provides ASN.1 helpers for building PKCS#7 structures.
|
|
// Used by EST (RFC 7030) and SCEP (RFC 8894) protocol handlers.
|
|
// No external dependencies — hand-rolled ASN.1 encoding only.
|
|
package pkcs7
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"fmt"
|
|
)
|
|
|
|
// BuildCertsOnlyPKCS7 creates a degenerate PKCS#7 SignedData structure containing only certificates.
|
|
// This is the "certs-only" format specified in RFC 7030 Section 4.1.3 for /cacerts responses
|
|
// and enrollment responses, and used by SCEP (RFC 8894) for GetCACert responses.
|
|
//
|
|
// ASN.1 structure (simplified):
|
|
//
|
|
// ContentInfo {
|
|
// contentType: signedData (1.2.840.113549.1.7.2)
|
|
// content: SignedData {
|
|
// version: 1
|
|
// digestAlgorithms: {} (empty)
|
|
// encapContentInfo: { contentType: data (1.2.840.113549.1.7.1) }
|
|
// certificates: [cert1, cert2, ...]
|
|
// signerInfos: {} (empty)
|
|
// }
|
|
// }
|
|
func BuildCertsOnlyPKCS7(derCerts [][]byte) ([]byte, error) {
|
|
// OID for signedData: 1.2.840.113549.1.7.2
|
|
oidSignedData := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02}
|
|
// OID for data: 1.2.840.113549.1.7.1
|
|
oidData := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01}
|
|
|
|
// Build certificates [0] IMPLICIT SET OF Certificate
|
|
var certsContent []byte
|
|
for _, cert := range derCerts {
|
|
certsContent = append(certsContent, cert...)
|
|
}
|
|
certsField := ASN1WrapImplicit(0, certsContent)
|
|
|
|
// Build encapContentInfo: SEQUENCE { OID data }
|
|
encapContentInfo := ASN1WrapSequence(oidData)
|
|
|
|
// Build digestAlgorithms: SET {} (empty)
|
|
digestAlgorithms := ASN1WrapSet(nil)
|
|
|
|
// Build signerInfos: SET {} (empty)
|
|
signerInfos := ASN1WrapSet(nil)
|
|
|
|
// Version: INTEGER 1
|
|
version := []byte{0x02, 0x01, 0x01}
|
|
|
|
// Build SignedData SEQUENCE
|
|
var signedDataContent []byte
|
|
signedDataContent = append(signedDataContent, version...)
|
|
signedDataContent = append(signedDataContent, digestAlgorithms...)
|
|
signedDataContent = append(signedDataContent, encapContentInfo...)
|
|
signedDataContent = append(signedDataContent, certsField...)
|
|
signedDataContent = append(signedDataContent, signerInfos...)
|
|
signedData := ASN1WrapSequence(signedDataContent)
|
|
|
|
// Wrap in [0] EXPLICIT for ContentInfo.content
|
|
contentField := ASN1WrapExplicit(0, signedData)
|
|
|
|
// Build ContentInfo SEQUENCE
|
|
var contentInfoContent []byte
|
|
contentInfoContent = append(contentInfoContent, oidSignedData...)
|
|
contentInfoContent = append(contentInfoContent, contentField...)
|
|
contentInfo := ASN1WrapSequence(contentInfoContent)
|
|
|
|
return contentInfo, nil
|
|
}
|
|
|
|
// PEMToDERChain converts PEM-encoded certificates to a slice of DER-encoded certificates.
|
|
func PEMToDERChain(pemData string) ([][]byte, error) {
|
|
var derCerts [][]byte
|
|
rest := []byte(pemData)
|
|
for {
|
|
var block *pem.Block
|
|
block, rest = pem.Decode(rest)
|
|
if block == nil {
|
|
break
|
|
}
|
|
if block.Type == "CERTIFICATE" {
|
|
derCerts = append(derCerts, block.Bytes)
|
|
}
|
|
}
|
|
if len(derCerts) == 0 {
|
|
return nil, fmt.Errorf("no certificates found in PEM data")
|
|
}
|
|
return derCerts, nil
|
|
}
|
|
|
|
// ASN1WrapSequence wraps content in an ASN.1 SEQUENCE tag (0x30).
|
|
func ASN1WrapSequence(content []byte) []byte {
|
|
return ASN1Wrap(0x30, content)
|
|
}
|
|
|
|
// ASN1WrapSet wraps content in an ASN.1 SET tag (0x31).
|
|
func ASN1WrapSet(content []byte) []byte {
|
|
return ASN1Wrap(0x31, content)
|
|
}
|
|
|
|
// ASN1WrapExplicit wraps content in an ASN.1 context-specific EXPLICIT tag.
|
|
func ASN1WrapExplicit(tag int, content []byte) []byte {
|
|
return ASN1Wrap(byte(0xa0|tag), content)
|
|
}
|
|
|
|
// ASN1WrapImplicit wraps content in an ASN.1 context-specific IMPLICIT CONSTRUCTED tag.
|
|
func ASN1WrapImplicit(tag int, content []byte) []byte {
|
|
return ASN1Wrap(byte(0xa0|tag), content)
|
|
}
|
|
|
|
// ASN1Wrap wraps content with an ASN.1 tag and length.
|
|
func ASN1Wrap(tag byte, content []byte) []byte {
|
|
length := len(content)
|
|
var result []byte
|
|
result = append(result, tag)
|
|
result = append(result, ASN1EncodeLength(length)...)
|
|
result = append(result, content...)
|
|
return result
|
|
}
|
|
|
|
// ASN1EncodeLength encodes a length in ASN.1 DER format.
|
|
func ASN1EncodeLength(length int) []byte {
|
|
if length < 0x80 {
|
|
return []byte{byte(length)}
|
|
}
|
|
// Long form
|
|
var lengthBytes []byte
|
|
l := length
|
|
for l > 0 {
|
|
lengthBytes = append([]byte{byte(l & 0xff)}, lengthBytes...)
|
|
l >>= 8
|
|
}
|
|
return append([]byte{byte(0x80 | len(lengthBytes))}, lengthBytes...)
|
|
}
|